Merge pull request #379 from Oloodi/361-missing-features-and-bugs-identified-during-the-milestone-3-demo-smoke-run
feat(mobile): Implement Centralized Error Handling System & Project Cleanup (#377 #378)
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
|
|
||||||
* In the mobile applications, since the structure is now finalized (at least for the existing features), we need to **strictly follow best practices while coding**:
|
* In the mobile applications, since the structure is now finalized (at least for the existing features), we need to **strictly follow best practices while coding**:
|
||||||
|
|
||||||
* Break down large widgets into **smaller, reusable widgets**
|
* Break down large widgets into **smaller, reusable widgets**
|
||||||
* Add **doc comments** where necessary to improve readability and maintainability
|
* Add **doc comments** where necessary to improve readability and maintainability
|
||||||
* **Remove overly complicated or unnecessary logic** introduced by AI and simplify where possible
|
* **Remove overly complicated or unnecessary logic** introduced by AI and simplify where possible
|
||||||
@@ -12,7 +11,41 @@
|
|||||||
- apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart
|
- apps/mobile/packages/features/client/client_coverage/lib/src/data/repositories_impl/coverage_repository_impl.dart
|
||||||
- Fix the location field in CoverageShiftRole to use the correct fallback logic.
|
- Fix the location field in CoverageShiftRole to use the correct fallback logic.
|
||||||
- line 125 remove redundant location values.
|
- line 125 remove redundant location values.
|
||||||
|
|
||||||
- Need to clarify the difference b/w `case dc.ApplicationStatus.ACCEPTED` and `case dc.ApplicationStatus.CONFIRMED`.
|
- Need to clarify the difference b/w `case dc.ApplicationStatus.ACCEPTED` and `case dc.ApplicationStatus.CONFIRMED`.
|
||||||
- Update the dataconnect docs.
|
- Update the dataconnect docs.
|
||||||
- Track `lat` and `lng` in the staff preferred work locations (for now we are only storing the name).
|
- Track `lat` and `lng` in the staff preferred work locations (for now we are only storing the name).
|
||||||
- Remove "Up Next (x)" counter from orders list in client app as it is confusing, becase the tab already has a badge showing the number of the upcoming orders.
|
|
||||||
|
- ` final String status;` in `OrderItem` make it an enum.
|
||||||
|
- /// Date of the shift (ISO format).
|
||||||
|
final String date; make this in the DateTime format instead of string.
|
||||||
|
|
||||||
|
- in `view_orders_cubit.dart` combine the logic of `_calculateUpNextCount ` and `_calculateTodayCount` into a single function that calculates both counts together to avoid redundant filtering of orders.
|
||||||
|
- In places api call in the when the api's not working we need to show a proper error message instead of just an empty list.
|
||||||
|
- pending should come first in the view order list.
|
||||||
|
- rename connect name to 'krow-connect' in the project.
|
||||||
|
|
||||||
|
- fix "dartdepend_on_referenced_packages" warnings.
|
||||||
|
- fix "dartunnecessary_library_name" warnings.
|
||||||
|
- fix lint issues.
|
||||||
|
- fix localization.
|
||||||
|
- centralise way to handle errors.
|
||||||
|
|
||||||
|
- track minimum shift hours in the staff profile and show a warning if they try to apply for shifts that are below their minimum hours.
|
||||||
|
- this need to be added in the BE and also a FE validation (5 hrs).
|
||||||
|
- Cannot cancel before 24 hours of the shift start time. If do we should charge for 4 hours of work for each shifts.
|
||||||
|
- verify the order creation process in the client app.
|
||||||
|
- Vendor don't need to verify the order, when the order is created it should be automatically published.
|
||||||
|
- rethink the order status, we need to simplify it.
|
||||||
|
- Validation layer
|
||||||
|
- Profile info
|
||||||
|
- emergency contact
|
||||||
|
- experiences
|
||||||
|
- attires
|
||||||
|
- there should be manual verification by the client even if the ai verification is passed.
|
||||||
|
- to track false positives and false negatives.
|
||||||
|
- certifications
|
||||||
|
- there should be manual verification by the client even if the ai verification is passed.
|
||||||
|
- to track false positives and false negatives.
|
||||||
|
- documents
|
||||||
|
- tax forms
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
|
import 'package:client_authentication/client_authentication.dart'
|
||||||
|
as client_authentication;
|
||||||
|
import 'package:client_create_order/client_create_order.dart'
|
||||||
|
as client_create_order;
|
||||||
|
import 'package:client_hubs/client_hubs.dart' as client_hubs;
|
||||||
|
import 'package:client_main/client_main.dart' as client_main;
|
||||||
|
import 'package:client_settings/client_settings.dart' as client_settings;
|
||||||
import 'package:core_localization/core_localization.dart' as core_localization;
|
import 'package:core_localization/core_localization.dart' as core_localization;
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:client_authentication/client_authentication.dart'
|
|
||||||
as client_authentication;
|
|
||||||
import 'package:client_main/client_main.dart' as client_main;
|
|
||||||
import 'package:client_settings/client_settings.dart' as client_settings;
|
|
||||||
import 'package:client_hubs/client_hubs.dart' as client_hubs;
|
|
||||||
import 'package:client_create_order/client_create_order.dart'
|
|
||||||
as client_create_order;
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -21,6 +22,13 @@ void main() async {
|
|||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null,
|
options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register global BLoC observer for centralized error logging
|
||||||
|
Bloc.observer = CoreBlocObserver(
|
||||||
|
logEvents: true,
|
||||||
|
logStateChanges: false, // Set to true for verbose debugging
|
||||||
|
);
|
||||||
|
|
||||||
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,23 +40,23 @@ class AppModule extends Module {
|
|||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
// Initial route points to the client authentication flow
|
// Initial route points to the client authentication flow
|
||||||
r.module('/', module: client_authentication.ClientAuthenticationModule());
|
r.module(ClientPaths.root, module: client_authentication.ClientAuthenticationModule());
|
||||||
|
|
||||||
// Client main shell with bottom navigation (includes home as a child)
|
// Client main shell with bottom navigation (includes home as a child)
|
||||||
r.module('/client-main', module: client_main.ClientMainModule());
|
r.module(ClientPaths.main, module: client_main.ClientMainModule());
|
||||||
|
|
||||||
// Client settings route
|
// Client settings route
|
||||||
r.module(
|
r.module(
|
||||||
'/client-settings',
|
ClientPaths.settings,
|
||||||
module: client_settings.ClientSettingsModule(),
|
module: client_settings.ClientSettingsModule(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Client hubs route
|
// Client hubs route
|
||||||
r.module('/client-hubs', module: client_hubs.ClientHubsModule());
|
r.module(ClientPaths.hubs, module: client_hubs.ClientHubsModule());
|
||||||
|
|
||||||
// Client create order route
|
// Client create order route
|
||||||
r.module(
|
r.module(
|
||||||
'/client/create-order',
|
ClientPaths.createOrder,
|
||||||
module: client_create_order.ClientCreateOrderModule(),
|
module: client_create_order.ClientCreateOrderModule(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ void main() async {
|
|||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Register global BLoC observer for centralized error logging
|
||||||
|
Bloc.observer = CoreBlocObserver(
|
||||||
|
logEvents: true,
|
||||||
|
logStateChanges: false, // Set to true for verbose debugging
|
||||||
|
);
|
||||||
|
|
||||||
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
runApp(ModularApp(module: AppModule(), child: const AppWidget()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,9 +34,9 @@ class AppModule extends Module {
|
|||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
// Set the initial route to the authentication module
|
// Set the initial route to the authentication module
|
||||||
r.module("/", module: staff_authentication.StaffAuthenticationModule());
|
r.module(StaffPaths.root, module: staff_authentication.StaffAuthenticationModule());
|
||||||
|
|
||||||
r.module('/worker-main', module: staff_main.StaffMainModule());
|
r.module(StaffPaths.main, module: staff_main.StaffMainModule());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ 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/presentation/mixins/bloc_error_handler.dart';
|
||||||
|
export 'src/presentation/observers/core_bloc_observer.dart';
|
||||||
|
export 'src/config/app_config.dart';
|
||||||
|
export 'src/routing/routing.dart';
|
||||||
|
|||||||
9
apps/mobile/packages/core/lib/src/config/app_config.dart
Normal file
9
apps/mobile/packages/core/lib/src/config/app_config.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// AppConfig class that holds configuration constants for the application.
|
||||||
|
/// This class is used to access various API keys and other configuration values
|
||||||
|
/// throughout the app.
|
||||||
|
class AppConfig {
|
||||||
|
AppConfig._();
|
||||||
|
|
||||||
|
/// The Google Places API key used for address autocomplete functionality.
|
||||||
|
static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY');
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Mixin to standardize error handling across all BLoCs.
|
||||||
|
///
|
||||||
|
/// This mixin provides a centralized way to handle errors in BLoC event handlers,
|
||||||
|
/// reducing boilerplate and ensuring consistent error handling patterns.
|
||||||
|
///
|
||||||
|
/// **Benefits:**
|
||||||
|
/// - Eliminates repetitive try-catch blocks
|
||||||
|
/// - Automatically logs errors with technical details
|
||||||
|
/// - Converts AppException to localized error keys
|
||||||
|
/// - Handles unexpected errors gracefully
|
||||||
|
///
|
||||||
|
/// **Usage:**
|
||||||
|
/// ```dart
|
||||||
|
/// class MyBloc extends Bloc<MyEvent, MyState> with BlocErrorHandler<MyState> {
|
||||||
|
/// Future<void> _onEvent(MyEvent event, Emitter<MyState> emit) async {
|
||||||
|
/// await handleError(
|
||||||
|
/// emit: emit,
|
||||||
|
/// action: () async {
|
||||||
|
/// final result = await _useCase();
|
||||||
|
/// emit(MyState.success(result));
|
||||||
|
/// },
|
||||||
|
/// onError: (errorKey) => MyState.error(errorKey),
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
mixin BlocErrorHandler<S> {
|
||||||
|
/// Executes an async action with centralized error handling.
|
||||||
|
///
|
||||||
|
/// [emit] - The state emitter from the event handler
|
||||||
|
/// [action] - The async operation to execute (e.g., calling a use case)
|
||||||
|
/// [onError] - Function that creates an error state from the error message key
|
||||||
|
/// [loggerName] - Optional custom logger name (defaults to BLoC class name)
|
||||||
|
///
|
||||||
|
/// **Error Flow:**
|
||||||
|
/// 1. Executes the action
|
||||||
|
/// 2. If AppException is thrown:
|
||||||
|
/// - Logs error code and technical message
|
||||||
|
/// - Emits error state with localization key
|
||||||
|
/// 3. If unexpected error is thrown:
|
||||||
|
/// - Logs full error and stack trace
|
||||||
|
/// - Emits generic error state
|
||||||
|
Future<void> handleError({
|
||||||
|
required Emitter<S> emit,
|
||||||
|
required Future<void> Function() action,
|
||||||
|
required S Function(String errorKey) onError,
|
||||||
|
String? loggerName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await action();
|
||||||
|
} on AppException catch (e) {
|
||||||
|
// Known application error - log technical details
|
||||||
|
developer.log(
|
||||||
|
'Error ${e.code}: ${e.technicalMessage}',
|
||||||
|
name: loggerName ?? runtimeType.toString(),
|
||||||
|
);
|
||||||
|
// Emit error state with localization key
|
||||||
|
emit(onError(e.messageKey));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
// Unexpected error - log everything for debugging
|
||||||
|
developer.log(
|
||||||
|
'Unexpected error: $e',
|
||||||
|
name: loggerName ?? runtimeType.toString(),
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
// Emit generic error state
|
||||||
|
emit(onError('errors.generic.unknown'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes an async action with error handling and returns a result.
|
||||||
|
///
|
||||||
|
/// This variant is useful when you need to get a value from the action
|
||||||
|
/// and handle errors without emitting states.
|
||||||
|
///
|
||||||
|
/// Returns the result of the action, or null if an error occurred.
|
||||||
|
///
|
||||||
|
/// **Usage:**
|
||||||
|
/// ```dart
|
||||||
|
/// final user = await handleErrorWithResult(
|
||||||
|
/// action: () => _getUserUseCase(),
|
||||||
|
/// onError: (errorKey) {
|
||||||
|
/// emit(MyState.error(errorKey));
|
||||||
|
/// },
|
||||||
|
/// );
|
||||||
|
/// if (user != null) {
|
||||||
|
/// emit(MyState.success(user));
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
Future<T?> handleErrorWithResult<T>({
|
||||||
|
required Future<T> Function() action,
|
||||||
|
required void Function(String errorKey) onError,
|
||||||
|
String? loggerName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await action();
|
||||||
|
} on AppException catch (e) {
|
||||||
|
developer.log(
|
||||||
|
'Error ${e.code}: ${e.technicalMessage}',
|
||||||
|
name: loggerName ?? runtimeType.toString(),
|
||||||
|
);
|
||||||
|
onError(e.messageKey);
|
||||||
|
return null;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
developer.log(
|
||||||
|
'Unexpected error: $e',
|
||||||
|
name: loggerName ?? runtimeType.toString(),
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
onError('errors.generic.unknown');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import 'dart:developer' as developer;
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
/// Global BLoC observer for centralized logging and monitoring.
|
||||||
|
///
|
||||||
|
/// This observer provides visibility into all BLoC lifecycle events across
|
||||||
|
/// the entire application, enabling centralized logging, debugging, and
|
||||||
|
/// error monitoring.
|
||||||
|
///
|
||||||
|
/// **Features:**
|
||||||
|
/// - Logs BLoC creation and disposal
|
||||||
|
/// - Logs all events and state changes
|
||||||
|
/// - Captures and logs errors with stack traces
|
||||||
|
/// - Ready for integration with monitoring services (Sentry, Firebase Crashlytics)
|
||||||
|
///
|
||||||
|
/// **Setup:**
|
||||||
|
/// Register this observer in your app's main.dart before runApp():
|
||||||
|
/// ```dart
|
||||||
|
/// void main() {
|
||||||
|
/// Bloc.observer = CoreBlocObserver();
|
||||||
|
/// runApp(MyApp());
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
class CoreBlocObserver extends BlocObserver {
|
||||||
|
/// Whether to log state changes (can be verbose in production)
|
||||||
|
final bool logStateChanges;
|
||||||
|
|
||||||
|
/// Whether to log events
|
||||||
|
final bool logEvents;
|
||||||
|
|
||||||
|
CoreBlocObserver({
|
||||||
|
this.logStateChanges = false,
|
||||||
|
this.logEvents = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onCreate(BlocBase bloc) {
|
||||||
|
super.onCreate(bloc);
|
||||||
|
developer.log(
|
||||||
|
'Created: ${bloc.runtimeType}',
|
||||||
|
name: 'BlocObserver',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onEvent(Bloc bloc, Object? event) {
|
||||||
|
super.onEvent(bloc, event);
|
||||||
|
if (logEvents) {
|
||||||
|
developer.log(
|
||||||
|
'Event: ${event.runtimeType}',
|
||||||
|
name: bloc.runtimeType.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChange(BlocBase bloc, Change change) {
|
||||||
|
super.onChange(bloc, change);
|
||||||
|
if (logStateChanges) {
|
||||||
|
developer.log(
|
||||||
|
'State: ${change.currentState.runtimeType} → ${change.nextState.runtimeType}',
|
||||||
|
name: bloc.runtimeType.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
|
||||||
|
super.onError(bloc, error, stackTrace);
|
||||||
|
|
||||||
|
// Log error to console
|
||||||
|
developer.log(
|
||||||
|
'ERROR in ${bloc.runtimeType}',
|
||||||
|
name: 'BlocObserver',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Send to monitoring service
|
||||||
|
// Example integrations:
|
||||||
|
//
|
||||||
|
// Sentry:
|
||||||
|
// Sentry.captureException(
|
||||||
|
// error,
|
||||||
|
// stackTrace: stackTrace,
|
||||||
|
// hint: Hint.withMap({'bloc': bloc.runtimeType.toString()}),
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// Firebase Crashlytics:
|
||||||
|
// FirebaseCrashlytics.instance.recordError(
|
||||||
|
// error,
|
||||||
|
// stackTrace,
|
||||||
|
// reason: 'BLoC Error in ${bloc.runtimeType}',
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose(BlocBase bloc) {
|
||||||
|
super.onClose(bloc);
|
||||||
|
developer.log(
|
||||||
|
'Closed: ${bloc.runtimeType}',
|
||||||
|
name: 'BlocObserver',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTransition(Bloc bloc, Transition transition) {
|
||||||
|
super.onTransition(bloc, transition);
|
||||||
|
if (logStateChanges) {
|
||||||
|
developer.log(
|
||||||
|
'Transition: ${transition.event.runtimeType} → ${transition.nextState.runtimeType}',
|
||||||
|
name: bloc.runtimeType.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
164
apps/mobile/packages/core/lib/src/routing/client/navigator.dart
Normal file
164
apps/mobile/packages/core/lib/src/routing/client/navigator.dart
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
|
import 'route_paths.dart';
|
||||||
|
|
||||||
|
/// Typed navigation extension for the Client application.
|
||||||
|
///
|
||||||
|
/// This extension provides type-safe navigation methods for all routes
|
||||||
|
/// in the Client app. All client navigation should use these methods
|
||||||
|
/// instead of hardcoding route strings.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
/// import 'package:krow_core/routing.dart';
|
||||||
|
///
|
||||||
|
/// // In your widget or bloc
|
||||||
|
/// Modular.to.toClientSignIn();
|
||||||
|
/// Modular.to.toClientHome();
|
||||||
|
/// Modular.to.toOrderDetails('order123');
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [ClientPaths] for route path constants
|
||||||
|
/// * [StaffNavigator] for Staff app navigation
|
||||||
|
extension ClientNavigator on IModularNavigator {
|
||||||
|
// ==========================================================================
|
||||||
|
// AUTHENTICATION FLOWS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Navigate to the root authentication screen.
|
||||||
|
///
|
||||||
|
/// This effectively logs out the user by navigating to root.
|
||||||
|
/// Used when signing out or session expires.
|
||||||
|
void toClientRoot() {
|
||||||
|
navigate(ClientPaths.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the client sign-in page.
|
||||||
|
///
|
||||||
|
/// This page allows existing clients to log in using email/password
|
||||||
|
/// or social authentication providers.
|
||||||
|
void toClientSignIn() {
|
||||||
|
pushNamed(ClientPaths.signIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the client sign-up page.
|
||||||
|
///
|
||||||
|
/// This page allows new clients to create an account and provides
|
||||||
|
/// the initial registration form.
|
||||||
|
void toClientSignUp() {
|
||||||
|
pushNamed(ClientPaths.signUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the client home dashboard.
|
||||||
|
///
|
||||||
|
/// This is typically called after successful authentication or when
|
||||||
|
/// returning to the main application from a deep feature.
|
||||||
|
///
|
||||||
|
/// Uses pushNamed to avoid trailing slash issues with navigate().
|
||||||
|
void toClientHome() {
|
||||||
|
navigate(ClientPaths.home);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the client main shell.
|
||||||
|
///
|
||||||
|
/// This is the container with bottom navigation. Usually you'd navigate
|
||||||
|
/// to a specific tab instead (like [toClientHome]).
|
||||||
|
void toClientMain() {
|
||||||
|
navigate(ClientPaths.main);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MAIN NAVIGATION TABS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Navigates to the Coverage tab.
|
||||||
|
///
|
||||||
|
/// Displays workforce coverage analytics and metrics.
|
||||||
|
void toClientCoverage() {
|
||||||
|
navigate(ClientPaths.coverage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the Billing tab.
|
||||||
|
///
|
||||||
|
/// Access billing history, invoices, and payment methods.
|
||||||
|
void toClientBilling() {
|
||||||
|
navigate(ClientPaths.billing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the Orders tab.
|
||||||
|
///
|
||||||
|
/// View and manage all shift orders with filtering and sorting.
|
||||||
|
void toClientOrders() {
|
||||||
|
navigate(ClientPaths.orders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the Reports tab.
|
||||||
|
///
|
||||||
|
/// Generate and view workforce reports and analytics.
|
||||||
|
void toClientReports() {
|
||||||
|
navigate(ClientPaths.reports);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SETTINGS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the client settings page.
|
||||||
|
///
|
||||||
|
/// Manage account settings, notifications, and app preferences.
|
||||||
|
void toClientSettings() {
|
||||||
|
pushNamed(ClientPaths.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// HUBS MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the client hubs management page.
|
||||||
|
///
|
||||||
|
/// View and manage physical locations/hubs where staff are deployed.
|
||||||
|
Future<void> toClientHubs() async {
|
||||||
|
await pushNamed(ClientPaths.hubs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// ORDER CREATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the order creation flow entry page.
|
||||||
|
///
|
||||||
|
/// This is the starting point for all order creation flows.
|
||||||
|
void toCreateOrder() {
|
||||||
|
pushNamed(ClientPaths.createOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the rapid order creation flow.
|
||||||
|
///
|
||||||
|
/// Quick shift creation with simplified inputs for urgent needs.
|
||||||
|
void toCreateOrderRapid() {
|
||||||
|
pushNamed(ClientPaths.createOrderRapid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the one-time order creation flow.
|
||||||
|
///
|
||||||
|
/// Create a shift that occurs once at a specific date and time.
|
||||||
|
void toCreateOrderOneTime() {
|
||||||
|
pushNamed(ClientPaths.createOrderOneTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the recurring order creation flow.
|
||||||
|
///
|
||||||
|
/// Create shifts that repeat on a defined schedule (daily, weekly, etc.).
|
||||||
|
void toCreateOrderRecurring() {
|
||||||
|
pushNamed(ClientPaths.createOrderRecurring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the permanent order creation flow.
|
||||||
|
///
|
||||||
|
/// Create a long-term or permanent staffing position.
|
||||||
|
void toCreateOrderPermanent() {
|
||||||
|
pushNamed(ClientPaths.createOrderPermanent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/// Centralized route path definitions for the KROW Client application.
|
||||||
|
///
|
||||||
|
/// This file contains all route paths used in the Client app, organized by feature.
|
||||||
|
/// All client navigation should reference these constants to ensure consistency
|
||||||
|
/// and make route changes easier to manage.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [StaffPaths] for Staff app routes
|
||||||
|
/// * [ClientNavigator] for typed navigation methods
|
||||||
|
class ClientPaths {
|
||||||
|
ClientPaths._();
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// CHILD ROUTE MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
/// Generate child route based on the given route and parent route
|
||||||
|
///
|
||||||
|
/// This is useful for creating nested routes within modules.
|
||||||
|
static String childRoute(String parent, String child) {
|
||||||
|
final String childPath = child.replaceFirst(parent, '');
|
||||||
|
|
||||||
|
// check if the child path is empty
|
||||||
|
if (childPath.isEmpty) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the child path starts with a '/'
|
||||||
|
if (!childPath.startsWith('/')) {
|
||||||
|
return '/$childPath';
|
||||||
|
}
|
||||||
|
|
||||||
|
return childPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// AUTHENTICATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Root path for the client authentication flow.
|
||||||
|
///
|
||||||
|
/// This serves as the entry point for unauthenticated users.
|
||||||
|
static const String root = '/';
|
||||||
|
|
||||||
|
/// Sign-in page where existing clients can log into their account.
|
||||||
|
///
|
||||||
|
/// Supports email/password and social authentication.
|
||||||
|
static const String signIn = '/client-sign-in';
|
||||||
|
|
||||||
|
/// Sign-up page where new clients can create an account.
|
||||||
|
///
|
||||||
|
/// Collects basic information and credentials for new client registration.
|
||||||
|
static const String signUp = '/client-sign-up';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MAIN SHELL & NAVIGATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Main shell route with bottom navigation.
|
||||||
|
///
|
||||||
|
/// This is the primary navigation container that hosts tabs for:
|
||||||
|
/// Home, Coverage, Billing, Orders, and Reports.
|
||||||
|
static const String main = '/client-main';
|
||||||
|
|
||||||
|
/// Home tab - the main dashboard for clients.
|
||||||
|
///
|
||||||
|
/// Displays quick actions, upcoming shifts, and recent activity.
|
||||||
|
static const String home = '/client-main/home';
|
||||||
|
|
||||||
|
/// Coverage tab - view coverage analytics and status.
|
||||||
|
///
|
||||||
|
/// Shows workforce coverage metrics and analytics.
|
||||||
|
static const String coverage = '/client-main/coverage';
|
||||||
|
|
||||||
|
/// Billing tab - manage billing and invoices.
|
||||||
|
///
|
||||||
|
/// Access billing history, payment methods, and invoices.
|
||||||
|
static const String billing = '/client-main/billing';
|
||||||
|
|
||||||
|
/// Orders tab - view and manage shift orders.
|
||||||
|
///
|
||||||
|
/// List of all orders with filtering and status tracking.
|
||||||
|
static const String orders = '/client-main/orders';
|
||||||
|
|
||||||
|
/// Reports tab - access various reports and analytics.
|
||||||
|
///
|
||||||
|
/// Generate and view workforce reports (placeholder).
|
||||||
|
static const String reports = '/client-main/reports';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SETTINGS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Client settings and preferences.
|
||||||
|
///
|
||||||
|
/// Manage account settings, notifications, and app preferences.
|
||||||
|
static const String settings = '/client-settings';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// HUBS MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Client hubs (locations) management.
|
||||||
|
///
|
||||||
|
/// View and manage physical locations/hubs where staff are deployed.
|
||||||
|
static const String hubs = '/client-hubs';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// ORDER CREATION & MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Base path for order creation flows.
|
||||||
|
///
|
||||||
|
/// Entry point for all order creation types.
|
||||||
|
static const String createOrder = '/create-order';
|
||||||
|
|
||||||
|
/// Rapid order creation - quick shift creation flow.
|
||||||
|
///
|
||||||
|
/// Simplified flow for creating single shifts quickly.
|
||||||
|
static const String createOrderRapid = '/create-order/rapid';
|
||||||
|
|
||||||
|
/// One-time order creation - single occurrence shift.
|
||||||
|
///
|
||||||
|
/// Create a shift that occurs once at a specific date/time.
|
||||||
|
static const String createOrderOneTime = '/create-order/one-time';
|
||||||
|
|
||||||
|
/// Recurring order creation - repeated shifts.
|
||||||
|
///
|
||||||
|
/// Create shifts that repeat on a schedule (daily, weekly, etc.).
|
||||||
|
static const String createOrderRecurring = '/create-order/recurring';
|
||||||
|
|
||||||
|
/// Permanent order creation - ongoing position.
|
||||||
|
///
|
||||||
|
/// Create a long-term or permanent staffing position.
|
||||||
|
static const String createOrderPermanent = '/create-order/permanent';
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
|
/// Base navigation utilities extension for [IModularNavigator].
|
||||||
|
///
|
||||||
|
/// Provides helper methods for common navigation patterns that can be used
|
||||||
|
/// across both Client and Staff applications. These utilities add error handling,
|
||||||
|
/// logging capabilities, and convenience methods on top of the base Modular
|
||||||
|
/// navigation API.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [ClientNavigator] for Client-specific navigation
|
||||||
|
/// * [StaffNavigator] for Staff-specific navigation
|
||||||
|
extension NavigationExtensions on IModularNavigator {
|
||||||
|
/// Safely navigates to a route with optional error handling.
|
||||||
|
///
|
||||||
|
/// This method wraps [navigate] with error handling to prevent navigation
|
||||||
|
/// failures from crashing the app.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// * [path] - The route path to navigate to
|
||||||
|
/// * [arguments] - Optional arguments to pass to the route
|
||||||
|
///
|
||||||
|
/// Returns `true` if navigation was successful, `false` otherwise.
|
||||||
|
Future<bool> safeNavigate(
|
||||||
|
String path, {
|
||||||
|
Object? arguments,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
navigate(path, arguments: arguments);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
// In production, you might want to log this to a monitoring service
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('Navigation error to $path: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safely pushes a named route with optional error handling.
|
||||||
|
///
|
||||||
|
/// This method wraps [pushNamed] with error handling to prevent navigation
|
||||||
|
/// failures from crashing the app.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// * [routeName] - The name of the route to push
|
||||||
|
/// * [arguments] - Optional arguments to pass to the route
|
||||||
|
///
|
||||||
|
/// Returns the result from the pushed route, or `null` if navigation failed.
|
||||||
|
Future<T?> safePush<T extends Object?>(
|
||||||
|
String routeName, {
|
||||||
|
Object? arguments,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await pushNamed<T>(routeName, arguments: arguments);
|
||||||
|
} catch (e) {
|
||||||
|
// In production, you might want to log this to a monitoring service
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('Push navigation error to $routeName: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pops all routes until reaching the root route.
|
||||||
|
///
|
||||||
|
/// This is useful for resetting the navigation stack, such as after logout
|
||||||
|
/// or when returning to the main entry point of the app.
|
||||||
|
void popToRoot() {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pops the current route if possible.
|
||||||
|
///
|
||||||
|
/// Returns `true` if a route was popped, `false` if already at root.
|
||||||
|
bool popSafe() {
|
||||||
|
if (canPop()) {
|
||||||
|
pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
apps/mobile/packages/core/lib/src/routing/routing.dart
Normal file
49
apps/mobile/packages/core/lib/src/routing/routing.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/// Centralized routing infrastructure for KROW applications.
|
||||||
|
///
|
||||||
|
/// This library provides a unified routing solution for both Client and Staff
|
||||||
|
/// applications, including:
|
||||||
|
///
|
||||||
|
/// * Route path constants organized by feature
|
||||||
|
/// * Type-safe navigation extensions
|
||||||
|
/// * Base navigation utilities
|
||||||
|
///
|
||||||
|
/// ## Usage
|
||||||
|
///
|
||||||
|
/// Import this library in your app code to access routing:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// import 'package:krow_core/routing.dart';
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### Client Navigation
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // In a Client app widget or bloc
|
||||||
|
/// Modular.to.toClientHome();
|
||||||
|
/// Modular.to.toCreateOrder();
|
||||||
|
/// Modular.to.toOrderDetails('order123');
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### Staff Navigation
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// // In a Staff app widget or bloc
|
||||||
|
/// Modular.to.toStaffHome();
|
||||||
|
/// Modular.to.toShiftDetails(shift);
|
||||||
|
/// Modular.to.toPhoneVerification(AuthMode.login);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### Direct Path Access
|
||||||
|
///
|
||||||
|
/// You can also access route paths directly:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// final homePath = ClientPaths.home;
|
||||||
|
/// final shiftsPath = StaffPaths.shifts;
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
export 'client/route_paths.dart';
|
||||||
|
export 'client/navigator.dart';
|
||||||
|
export 'staff/route_paths.dart';
|
||||||
|
export 'staff/navigator.dart';
|
||||||
|
export 'navigation_extensions.dart';
|
||||||
300
apps/mobile/packages/core/lib/src/routing/staff/navigator.dart
Normal file
300
apps/mobile/packages/core/lib/src/routing/staff/navigator.dart
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'route_paths.dart';
|
||||||
|
|
||||||
|
/// Typed navigation extension for the Staff application.
|
||||||
|
///
|
||||||
|
/// This extension provides type-safe navigation methods for all routes
|
||||||
|
/// in the Staff app. All staff navigation should use these methods
|
||||||
|
/// instead of hardcoding route strings.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
/// import 'package:krow_core/routing.dart';
|
||||||
|
///
|
||||||
|
/// // In your widget or bloc
|
||||||
|
/// Modular.to.toStaffHome();
|
||||||
|
/// Modular.to.toShiftDetails(shift);
|
||||||
|
/// Modular.to.toPhoneVerification('login'); // 'login' or 'signup'
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [StaffPaths] for route path constants
|
||||||
|
/// * [ClientNavigator] for Client app navigation
|
||||||
|
extension StaffNavigator on IModularNavigator {
|
||||||
|
// ==========================================================================
|
||||||
|
// AUTHENTICATION FLOWS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Navigates to the root get started/authentication screen.
|
||||||
|
///
|
||||||
|
/// This effectively logs out the user by navigating to root.
|
||||||
|
/// Used when signing out or session expires.
|
||||||
|
void toGetStarted() {
|
||||||
|
navigate(StaffPaths.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the phone verification page.
|
||||||
|
///
|
||||||
|
/// Used for both login and signup flows to verify phone numbers via OTP.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// * [mode] - The authentication mode: 'login' or 'signup'
|
||||||
|
///
|
||||||
|
/// The mode is passed as an argument and used by the verification page
|
||||||
|
/// to determine the appropriate flow.
|
||||||
|
void toPhoneVerification(String mode) {
|
||||||
|
pushNamed(
|
||||||
|
StaffPaths.phoneVerification,
|
||||||
|
arguments: <String, String>{'mode': mode},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the profile setup page, replacing the current route.
|
||||||
|
///
|
||||||
|
/// This is typically called after successful phone verification for new
|
||||||
|
/// staff members. Uses pushReplacement to prevent going back to verification.
|
||||||
|
void toProfileSetup() {
|
||||||
|
pushReplacementNamed(StaffPaths.profileSetup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MAIN NAVIGATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Navigates to the staff home dashboard.
|
||||||
|
///
|
||||||
|
/// This is the main landing page for authenticated staff members.
|
||||||
|
/// Displays shift cards, quick actions, and notifications.
|
||||||
|
void toStaffHome() {
|
||||||
|
pushNamed(StaffPaths.home);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the staff main shell.
|
||||||
|
///
|
||||||
|
/// This is the container with bottom navigation. Navigates to home tab
|
||||||
|
/// by default. Usually you'd navigate to a specific tab instead.
|
||||||
|
void toStaffMain() {
|
||||||
|
navigate('${StaffPaths.main}/home/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MAIN NAVIGATION TABS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Navigates to the Shifts tab.
|
||||||
|
///
|
||||||
|
/// Browse available shifts, accepted shifts, and shift history.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// * [selectedDate] - Optional date to pre-select in the shifts view
|
||||||
|
/// * [initialTab] - Optional initial tab (via query parameter)
|
||||||
|
void toShifts({DateTime? selectedDate, String? initialTab}) {
|
||||||
|
final Map<String, dynamic> args = <String, dynamic>{};
|
||||||
|
if (selectedDate != null) {
|
||||||
|
args['selectedDate'] = selectedDate;
|
||||||
|
}
|
||||||
|
if (initialTab != null) {
|
||||||
|
args['initialTab'] = initialTab;
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
StaffPaths.shifts,
|
||||||
|
arguments: args.isEmpty ? null : args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the Payments tab.
|
||||||
|
///
|
||||||
|
/// View payment history, earnings breakdown, and tax information.
|
||||||
|
void toPayments() {
|
||||||
|
navigate(StaffPaths.payments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the Clock In tab.
|
||||||
|
///
|
||||||
|
/// Access time tracking interface for active shifts.
|
||||||
|
void toClockIn() {
|
||||||
|
navigate(StaffPaths.clockIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the Profile tab.
|
||||||
|
///
|
||||||
|
/// Manage personal information, documents, and preferences.
|
||||||
|
void toProfile() {
|
||||||
|
navigate(StaffPaths.profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SHIFT MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Navigates to the shift details page for a specific shift.
|
||||||
|
///
|
||||||
|
/// Displays comprehensive information about a shift including location,
|
||||||
|
/// time, pay rate, and action buttons for accepting/declining/applying.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
/// * [shift] - The shift entity to display details for
|
||||||
|
///
|
||||||
|
/// The shift object is passed as an argument and can be retrieved
|
||||||
|
/// in the details page.
|
||||||
|
void toShiftDetails(Shift shift) {
|
||||||
|
navigate(
|
||||||
|
StaffPaths.shiftDetails(shift.id),
|
||||||
|
arguments: shift,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the shift details page (alternative method).
|
||||||
|
///
|
||||||
|
/// Same as [toShiftDetails] but using pushNamed instead of navigate.
|
||||||
|
/// Use this when you want to add the details page to the stack rather
|
||||||
|
/// than replacing the current route.
|
||||||
|
void pushShiftDetails(Shift shift) {
|
||||||
|
pushNamed(
|
||||||
|
StaffPaths.shiftDetails(shift.id),
|
||||||
|
arguments: shift,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// ONBOARDING & PROFILE SECTIONS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the personal information page.
|
||||||
|
///
|
||||||
|
/// Collect or edit basic personal information.
|
||||||
|
void toPersonalInfo() {
|
||||||
|
pushNamed(StaffPaths.onboardingPersonalInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the emergency contact page.
|
||||||
|
///
|
||||||
|
/// Manage emergency contact details for safety purposes.
|
||||||
|
void toEmergencyContact() {
|
||||||
|
pushNamed(StaffPaths.emergencyContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the work experience page.
|
||||||
|
///
|
||||||
|
/// Record previous work experience and qualifications.
|
||||||
|
void toExperience() {
|
||||||
|
pushNamed(StaffPaths.experience);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the attire preferences page.
|
||||||
|
///
|
||||||
|
/// Record sizing and appearance information for uniform allocation.
|
||||||
|
void toAttire() {
|
||||||
|
pushNamed(StaffPaths.attire);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// COMPLIANCE & DOCUMENTS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the documents management page.
|
||||||
|
///
|
||||||
|
/// Upload and manage required documents like ID and work permits.
|
||||||
|
void toDocuments() {
|
||||||
|
pushNamed(StaffPaths.documents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the certificates management page.
|
||||||
|
///
|
||||||
|
/// Manage professional certificates (e.g., food handling, CPR).
|
||||||
|
void toCertificates() {
|
||||||
|
pushNamed(StaffPaths.certificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// FINANCIAL INFORMATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the bank account information page.
|
||||||
|
///
|
||||||
|
/// Manage banking details for direct deposit payments.
|
||||||
|
void toBankAccount() {
|
||||||
|
pushNamed(StaffPaths.bankAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the tax forms page.
|
||||||
|
///
|
||||||
|
/// Manage W-4, tax withholding, and related tax documents.
|
||||||
|
void toTaxForms() {
|
||||||
|
pushNamed(StaffPaths.taxForms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the time card page.
|
||||||
|
///
|
||||||
|
/// View detailed time entries and timesheets.
|
||||||
|
void toTimeCard() {
|
||||||
|
pushNamed(StaffPaths.timeCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SCHEDULING & AVAILABILITY
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the availability management page.
|
||||||
|
///
|
||||||
|
/// Define when the staff member is available to work.
|
||||||
|
void toAvailability() {
|
||||||
|
pushNamed(StaffPaths.availability);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// ADDITIONAL FEATURES
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Pushes the Krow University page (placeholder).
|
||||||
|
///
|
||||||
|
/// Access training materials and educational courses.
|
||||||
|
void toKrowUniversity() {
|
||||||
|
pushNamed(StaffPaths.krowUniversity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the trainings page (placeholder).
|
||||||
|
///
|
||||||
|
/// View and complete required training modules.
|
||||||
|
void toTrainings() {
|
||||||
|
pushNamed(StaffPaths.trainings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the leaderboard page (placeholder).
|
||||||
|
///
|
||||||
|
/// View performance rankings and achievements.
|
||||||
|
void toLeaderboard() {
|
||||||
|
pushNamed(StaffPaths.leaderboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the FAQs page.
|
||||||
|
///
|
||||||
|
/// Access frequently asked questions and help resources.
|
||||||
|
void toFaqs() {
|
||||||
|
pushNamed(StaffPaths.faqs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the privacy and security settings page.
|
||||||
|
///
|
||||||
|
/// Manage privacy preferences and security settings.
|
||||||
|
void toPrivacy() {
|
||||||
|
pushNamed(StaffPaths.privacy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the messages page (placeholder).
|
||||||
|
///
|
||||||
|
/// Access internal messaging system.
|
||||||
|
void toMessages() {
|
||||||
|
pushNamed(StaffPaths.messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the settings page (placeholder).
|
||||||
|
///
|
||||||
|
/// General app settings and preferences.
|
||||||
|
void toSettings() {
|
||||||
|
pushNamed(StaffPaths.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
211
apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart
Normal file
211
apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
/// Centralized route path definitions for the KROW Staff application.
|
||||||
|
///
|
||||||
|
/// This file contains all route paths used in the Staff app, organized by feature.
|
||||||
|
/// All staff navigation should reference these constants to ensure consistency
|
||||||
|
/// and make route changes easier to manage.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [ClientPaths] for Client app routes
|
||||||
|
/// * [StaffNavigator] for typed navigation methods
|
||||||
|
class StaffPaths {
|
||||||
|
StaffPaths._();
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// CHILD ROUTE MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
/// Generate child route based on the given route and parent route
|
||||||
|
///
|
||||||
|
/// This is useful for creating nested routes within modules.
|
||||||
|
static String childRoute(String parent, String child) {
|
||||||
|
final String childPath = child.replaceFirst(parent, '');
|
||||||
|
|
||||||
|
// check if the child path is empty
|
||||||
|
if (childPath.isEmpty) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the child path starts with a '/'
|
||||||
|
if (!childPath.startsWith('/')) {
|
||||||
|
return '/$childPath';
|
||||||
|
}
|
||||||
|
|
||||||
|
return childPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// AUTHENTICATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Root path for the staff authentication flow.
|
||||||
|
///
|
||||||
|
/// This serves as the entry point for unauthenticated staff members.
|
||||||
|
static const String root = '/';
|
||||||
|
|
||||||
|
/// Phone verification page (relative path within auth module).
|
||||||
|
///
|
||||||
|
/// Used for both login and signup flows to verify phone numbers via OTP.
|
||||||
|
/// Expects `mode` argument: 'login' or 'signup'
|
||||||
|
static const String phoneVerification = '/phone-verification';
|
||||||
|
|
||||||
|
/// Profile setup page (relative path within auth module).
|
||||||
|
///
|
||||||
|
/// Initial profile setup for new staff members after verification.
|
||||||
|
static const String profileSetup = '/profile-setup';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// MAIN SHELL & NAVIGATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Main shell route with bottom navigation.
|
||||||
|
///
|
||||||
|
/// This is the primary navigation container that hosts tabs for:
|
||||||
|
/// Shifts, Payments, Home, Clock In, and Profile.
|
||||||
|
static const String main = '/worker-main';
|
||||||
|
|
||||||
|
/// Home tab - the main dashboard for staff.
|
||||||
|
///
|
||||||
|
/// Displays shift cards, quick actions, and notifications.
|
||||||
|
static const String home = '/worker-main/home';
|
||||||
|
|
||||||
|
/// Shifts tab - view and manage shifts.
|
||||||
|
///
|
||||||
|
/// Browse available shifts, accepted shifts, and shift history.
|
||||||
|
static const String shifts = '/worker-main/shifts';
|
||||||
|
|
||||||
|
/// Payments tab - view payment history and earnings.
|
||||||
|
///
|
||||||
|
/// Access payment history, earnings breakdown, and tax information.
|
||||||
|
static const String payments = '/worker-main/payments';
|
||||||
|
|
||||||
|
/// Clock In tab - clock in/out functionality.
|
||||||
|
///
|
||||||
|
/// Time tracking interface for active shifts.
|
||||||
|
static const String clockIn = '/worker-main/clock-in';
|
||||||
|
|
||||||
|
/// Profile tab - staff member profile and settings.
|
||||||
|
///
|
||||||
|
/// Manage personal information, documents, and preferences.
|
||||||
|
static const String profile = '/worker-main/profile';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SHIFT MANAGEMENT
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Shift details route.
|
||||||
|
///
|
||||||
|
/// View detailed information for a specific shift.
|
||||||
|
static const String shiftDetailsRoute = '/worker-main/shift-details';
|
||||||
|
|
||||||
|
/// Shift details page (dynamic).
|
||||||
|
///
|
||||||
|
/// View detailed information for a specific shift.
|
||||||
|
/// Path format: `/worker-main/shift-details/{shiftId}`
|
||||||
|
///
|
||||||
|
/// Example: `/worker-main/shift-details/shift123`
|
||||||
|
static String shiftDetails(String shiftId) =>
|
||||||
|
'$shiftDetailsRoute/$shiftId';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// ONBOARDING & PROFILE SECTIONS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Personal information onboarding.
|
||||||
|
///
|
||||||
|
/// Collect basic personal information during staff onboarding.
|
||||||
|
static const String onboardingPersonalInfo =
|
||||||
|
'/worker-main/onboarding/personal-info';
|
||||||
|
|
||||||
|
/// Emergency contact information.
|
||||||
|
///
|
||||||
|
/// Manage emergency contact details for safety purposes.
|
||||||
|
static const String emergencyContact = '/worker-main/emergency-contact';
|
||||||
|
|
||||||
|
/// Work experience information.
|
||||||
|
///
|
||||||
|
/// Record previous work experience and qualifications.
|
||||||
|
static const String experience = '/worker-main/experience';
|
||||||
|
|
||||||
|
/// Attire and appearance preferences.
|
||||||
|
///
|
||||||
|
/// Record sizing and appearance information for uniform allocation.
|
||||||
|
static const String attire = '/worker-main/attire';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// COMPLIANCE & DOCUMENTS
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Documents management - upload and manage required documents.
|
||||||
|
///
|
||||||
|
/// Store ID, work permits, and other required documentation.
|
||||||
|
static const String documents = '/worker-main/documents';
|
||||||
|
|
||||||
|
/// Certificates management - professional certifications.
|
||||||
|
///
|
||||||
|
/// Manage professional certificates (e.g., food handling, CPR, etc.).
|
||||||
|
static const String certificates = '/worker-main/certificates';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// FINANCIAL INFORMATION
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Bank account information for direct deposit.
|
||||||
|
///
|
||||||
|
/// Manage banking details for payment processing.
|
||||||
|
static const String bankAccount = '/worker-main/bank-account';
|
||||||
|
|
||||||
|
/// Tax forms and withholding information.
|
||||||
|
///
|
||||||
|
/// Manage W-4, tax withholding, and related tax documents.
|
||||||
|
static const String taxForms = '/worker-main/tax-forms';
|
||||||
|
|
||||||
|
/// Form I-9 - Employment Eligibility Verification.
|
||||||
|
///
|
||||||
|
/// Complete and manage I-9 employment verification form.
|
||||||
|
static const String formI9 = '/worker-main/tax-forms/i9';
|
||||||
|
|
||||||
|
/// Form W-4 - Employee's Withholding Certificate.
|
||||||
|
///
|
||||||
|
/// Complete and manage W-4 tax withholding form.
|
||||||
|
static const String formW4 = '/worker-main/tax-forms/w4';
|
||||||
|
|
||||||
|
/// Time card - view detailed time tracking records.
|
||||||
|
///
|
||||||
|
/// Access detailed time entries and timesheets.
|
||||||
|
static const String timeCard = '/worker-main/time-card';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// SCHEDULING & AVAILABILITY
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Availability management - set working hours preferences.
|
||||||
|
///
|
||||||
|
/// Define when the staff member is available to work.
|
||||||
|
static const String availability = '/worker-main/availability';
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// ADDITIONAL FEATURES (Placeholders)
|
||||||
|
// ==========================================================================
|
||||||
|
|
||||||
|
/// Krow University - training and education (placeholder).
|
||||||
|
///
|
||||||
|
/// Access to training materials and courses.
|
||||||
|
static const String krowUniversity = '/krow-university';
|
||||||
|
|
||||||
|
/// Training modules (placeholder).
|
||||||
|
static const String trainings = '/trainings';
|
||||||
|
|
||||||
|
/// Leaderboard - performance rankings (placeholder).
|
||||||
|
static const String leaderboard = '/leaderboard';
|
||||||
|
|
||||||
|
/// FAQs - frequently asked questions.
|
||||||
|
static const String faqs = '/faqs';
|
||||||
|
|
||||||
|
/// Privacy and security settings.
|
||||||
|
static const String privacy = '/privacy';
|
||||||
|
|
||||||
|
/// Messages - internal messaging system (placeholder).
|
||||||
|
static const String messages = '/messages';
|
||||||
|
|
||||||
|
/// General settings (placeholder).
|
||||||
|
static const String settings = '/settings';
|
||||||
|
}
|
||||||
@@ -13,3 +13,5 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
design_system:
|
design_system:
|
||||||
path: ../design_system
|
path: ../design_system
|
||||||
|
equatable: ^2.0.8
|
||||||
|
flutter_modular: ^6.4.1
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ export 'src/widgets/ui_step_indicator.dart';
|
|||||||
export 'src/widgets/ui_icon_button.dart';
|
export 'src/widgets/ui_icon_button.dart';
|
||||||
export 'src/widgets/ui_button.dart';
|
export 'src/widgets/ui_button.dart';
|
||||||
export 'src/widgets/ui_chip.dart';
|
export 'src/widgets/ui_chip.dart';
|
||||||
|
export 'src/widgets/ui_error_snackbar.dart';
|
||||||
|
export 'src/widgets/ui_success_snackbar.dart';
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_core_localization/krow_core_localization.dart';
|
||||||
|
import '../ui_colors.dart';
|
||||||
|
import '../ui_typography.dart';
|
||||||
|
|
||||||
|
/// Centralized error snackbar for consistent error presentation across the app.
|
||||||
|
///
|
||||||
|
/// This widget automatically resolves localization keys and displays
|
||||||
|
/// user-friendly error messages with optional error codes for support.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// UiErrorSnackbar.show(
|
||||||
|
/// context,
|
||||||
|
/// messageKey: 'errors.auth.invalid_credentials',
|
||||||
|
/// errorCode: 'AUTH_001',
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
class UiErrorSnackbar {
|
||||||
|
/// Shows an error snackbar with a localized message.
|
||||||
|
///
|
||||||
|
/// [messageKey] should be a dot-separated path like 'errors.auth.invalid_credentials'
|
||||||
|
/// [errorCode] is optional and will be shown in smaller text for support reference
|
||||||
|
/// [duration] controls how long the snackbar is visible
|
||||||
|
static void show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String messageKey,
|
||||||
|
String? errorCode,
|
||||||
|
Duration duration = const Duration(seconds: 4),
|
||||||
|
}) {
|
||||||
|
final texts = Texts.of(context);
|
||||||
|
final message = _getMessageFromKey(texts, messageKey);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: UiColors.white),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: UiTypography.body2m.copyWith(color: UiColors.white),
|
||||||
|
),
|
||||||
|
if (errorCode != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Error Code: $errorCode',
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.white.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: UiColors.error,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
duration: duration,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a localization key path to the actual translated message.
|
||||||
|
///
|
||||||
|
/// Supports keys like:
|
||||||
|
/// - errors.auth.invalid_credentials
|
||||||
|
/// - errors.hub.has_orders
|
||||||
|
/// - errors.generic.unknown
|
||||||
|
static String _getMessageFromKey(Texts texts, String key) {
|
||||||
|
// Parse key like "errors.auth.invalid_credentials"
|
||||||
|
final parts = key.split('.');
|
||||||
|
if (parts.length < 2) return texts.errors.generic.unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (parts[1]) {
|
||||||
|
case 'auth':
|
||||||
|
return _getAuthError(texts, parts.length > 2 ? parts[2] : '');
|
||||||
|
case 'hub':
|
||||||
|
return _getHubError(texts, parts.length > 2 ? parts[2] : '');
|
||||||
|
case 'order':
|
||||||
|
return _getOrderError(texts, parts.length > 2 ? parts[2] : '');
|
||||||
|
case 'profile':
|
||||||
|
return _getProfileError(texts, parts.length > 2 ? parts[2] : '');
|
||||||
|
case 'shift':
|
||||||
|
return _getShiftError(texts, parts.length > 2 ? parts[2] : '');
|
||||||
|
case 'generic':
|
||||||
|
return _getGenericError(texts, parts.length > 2 ? parts[2] : '');
|
||||||
|
default:
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getAuthError(Texts texts, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'invalid_credentials':
|
||||||
|
return texts.errors.auth.invalid_credentials;
|
||||||
|
case 'account_exists':
|
||||||
|
return texts.errors.auth.account_exists;
|
||||||
|
case 'session_expired':
|
||||||
|
return texts.errors.auth.session_expired;
|
||||||
|
case 'user_not_found':
|
||||||
|
return texts.errors.auth.user_not_found;
|
||||||
|
case 'unauthorized_app':
|
||||||
|
return texts.errors.auth.unauthorized_app;
|
||||||
|
case 'weak_password':
|
||||||
|
return texts.errors.auth.weak_password;
|
||||||
|
case 'sign_up_failed':
|
||||||
|
return texts.errors.auth.sign_up_failed;
|
||||||
|
case 'sign_in_failed':
|
||||||
|
return texts.errors.auth.sign_in_failed;
|
||||||
|
case 'not_authenticated':
|
||||||
|
return texts.errors.auth.not_authenticated;
|
||||||
|
case 'password_mismatch':
|
||||||
|
return texts.errors.auth.password_mismatch;
|
||||||
|
case 'google_only_account':
|
||||||
|
return texts.errors.auth.google_only_account;
|
||||||
|
default:
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getHubError(Texts texts, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'has_orders':
|
||||||
|
return texts.errors.hub.has_orders;
|
||||||
|
case 'not_found':
|
||||||
|
return texts.errors.hub.not_found;
|
||||||
|
case 'creation_failed':
|
||||||
|
return texts.errors.hub.creation_failed;
|
||||||
|
default:
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getOrderError(Texts texts, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'missing_hub':
|
||||||
|
return texts.errors.order.missing_hub;
|
||||||
|
case 'missing_vendor':
|
||||||
|
return texts.errors.order.missing_vendor;
|
||||||
|
case 'creation_failed':
|
||||||
|
return texts.errors.order.creation_failed;
|
||||||
|
case 'shift_creation_failed':
|
||||||
|
return texts.errors.order.shift_creation_failed;
|
||||||
|
case 'missing_business':
|
||||||
|
return texts.errors.order.missing_business;
|
||||||
|
default:
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getProfileError(Texts texts, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'staff_not_found':
|
||||||
|
return texts.errors.profile.staff_not_found;
|
||||||
|
case 'business_not_found':
|
||||||
|
return texts.errors.profile.business_not_found;
|
||||||
|
case 'update_failed':
|
||||||
|
return texts.errors.profile.update_failed;
|
||||||
|
default:
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getShiftError(Texts texts, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'no_open_roles':
|
||||||
|
return texts.errors.shift.no_open_roles;
|
||||||
|
case 'application_not_found':
|
||||||
|
return texts.errors.shift.application_not_found;
|
||||||
|
case 'no_active_shift':
|
||||||
|
return texts.errors.shift.no_active_shift;
|
||||||
|
default:
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _getGenericError(Texts texts, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'unknown':
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
case 'no_connection':
|
||||||
|
return texts.errors.generic.no_connection;
|
||||||
|
default:
|
||||||
|
return texts.errors.generic.unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../ui_colors.dart';
|
||||||
|
import '../ui_typography.dart';
|
||||||
|
|
||||||
|
/// Centralized success snackbar for consistent success message presentation.
|
||||||
|
///
|
||||||
|
/// This widget provides a unified way to show success feedback across the app
|
||||||
|
/// with consistent styling and behavior.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```dart
|
||||||
|
/// UiSuccessSnackbar.show(
|
||||||
|
/// context,
|
||||||
|
/// message: 'Profile updated successfully!',
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
class UiSuccessSnackbar {
|
||||||
|
/// Shows a success snackbar with a custom message.
|
||||||
|
///
|
||||||
|
/// [message] is the success message to display
|
||||||
|
/// [duration] controls how long the snackbar is visible
|
||||||
|
static void show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String message,
|
||||||
|
Duration duration = const Duration(seconds: 3),
|
||||||
|
}) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle_outline, color: UiColors.white),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: UiTypography.body2m.copyWith(color: UiColors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: UiColors.success,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
duration: duration,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ library client_authentication;
|
|||||||
|
|
||||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'src/data/repositories_impl/auth_repository_impl.dart';
|
import 'src/data/repositories_impl/auth_repository_impl.dart';
|
||||||
import 'src/domain/repositories/auth_repository_interface.dart';
|
import 'src/domain/repositories/auth_repository_interface.dart';
|
||||||
@@ -17,7 +18,6 @@ import 'src/presentation/pages/client_sign_up_page.dart';
|
|||||||
export 'src/presentation/pages/client_get_started_page.dart';
|
export 'src/presentation/pages/client_get_started_page.dart';
|
||||||
export 'src/presentation/pages/client_sign_in_page.dart';
|
export 'src/presentation/pages/client_sign_in_page.dart';
|
||||||
export 'src/presentation/pages/client_sign_up_page.dart';
|
export 'src/presentation/pages/client_sign_up_page.dart';
|
||||||
export 'src/presentation/navigation/client_auth_navigator.dart';
|
|
||||||
export 'package:core_localization/core_localization.dart';
|
export 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
/// A [Module] for the client authentication feature.
|
/// A [Module] for the client authentication feature.
|
||||||
@@ -60,8 +60,8 @@ class ClientAuthenticationModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const ClientGetStartedPage());
|
r.child(ClientPaths.root, child: (_) => const ClientGetStartedPage());
|
||||||
r.child('/client-sign-in', child: (_) => const ClientSignInPage());
|
r.child(ClientPaths.signIn, child: (_) => const ClientSignInPage());
|
||||||
r.child('/client-sign-up', child: (_) => const ClientSignUpPage());
|
r.child(ClientPaths.signUp, child: (_) => const ClientSignUpPage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:developer' as developer;
|
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import '../../domain/arguments/sign_in_with_email_arguments.dart';
|
import '../../domain/arguments/sign_in_with_email_arguments.dart';
|
||||||
@@ -24,7 +23,8 @@ import 'client_auth_state.dart';
|
|||||||
/// * Business Account Registration
|
/// * Business Account Registration
|
||||||
/// * Social Authentication
|
/// * Social Authentication
|
||||||
/// * Session Termination
|
/// * Session Termination
|
||||||
class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
|
||||||
|
with BlocErrorHandler<ClientAuthState> {
|
||||||
final SignInWithEmailUseCase _signInWithEmail;
|
final SignInWithEmailUseCase _signInWithEmail;
|
||||||
final SignUpWithEmailUseCase _signUpWithEmail;
|
final SignUpWithEmailUseCase _signUpWithEmail;
|
||||||
final SignInWithSocialUseCase _signInWithSocial;
|
final SignInWithSocialUseCase _signInWithSocial;
|
||||||
@@ -53,28 +53,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
|||||||
Emitter<ClientAuthState> emit,
|
Emitter<ClientAuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientAuthStatus.loading));
|
emit(state.copyWith(status: ClientAuthStatus.loading));
|
||||||
try {
|
|
||||||
final User user = await _signInWithEmail(
|
await handleError(
|
||||||
SignInWithEmailArguments(email: event.email, password: event.password),
|
emit: emit,
|
||||||
);
|
action: () async {
|
||||||
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
final user = await _signInWithEmail(
|
||||||
} on AppException catch (e) {
|
SignInWithEmailArguments(email: event.email, password: event.password),
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
);
|
||||||
emit(
|
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientAuthStatus.error,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientAuthStatus.error,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientAuthStatus.error,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the [ClientSignUpRequested] event.
|
/// Handles the [ClientSignUpRequested] event.
|
||||||
@@ -83,32 +75,24 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
|||||||
Emitter<ClientAuthState> emit,
|
Emitter<ClientAuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientAuthStatus.loading));
|
emit(state.copyWith(status: ClientAuthStatus.loading));
|
||||||
try {
|
|
||||||
final User user = await _signUpWithEmail(
|
await handleError(
|
||||||
SignUpWithEmailArguments(
|
emit: emit,
|
||||||
companyName: event.companyName,
|
action: () async {
|
||||||
email: event.email,
|
final user = await _signUpWithEmail(
|
||||||
password: event.password,
|
SignUpWithEmailArguments(
|
||||||
),
|
companyName: event.companyName,
|
||||||
);
|
email: event.email,
|
||||||
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
password: event.password,
|
||||||
} on AppException catch (e) {
|
),
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
);
|
||||||
emit(
|
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientAuthStatus.error,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientAuthStatus.error,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientAuthStatus.error,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the [ClientSocialSignInRequested] event.
|
/// Handles the [ClientSocialSignInRequested] event.
|
||||||
@@ -117,28 +101,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
|||||||
Emitter<ClientAuthState> emit,
|
Emitter<ClientAuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientAuthStatus.loading));
|
emit(state.copyWith(status: ClientAuthStatus.loading));
|
||||||
try {
|
|
||||||
final User user = await _signInWithSocial(
|
await handleError(
|
||||||
SignInWithSocialArguments(provider: event.provider),
|
emit: emit,
|
||||||
);
|
action: () async {
|
||||||
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
final user = await _signInWithSocial(
|
||||||
} on AppException catch (e) {
|
SignInWithSocialArguments(provider: event.provider),
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
);
|
||||||
emit(
|
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientAuthStatus.error,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientAuthStatus.error,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientAuthStatus.error,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the [ClientSignOutRequested] event.
|
/// Handles the [ClientSignOutRequested] event.
|
||||||
@@ -147,25 +123,17 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
|||||||
Emitter<ClientAuthState> emit,
|
Emitter<ClientAuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientAuthStatus.loading));
|
emit(state.copyWith(status: ClientAuthStatus.loading));
|
||||||
try {
|
|
||||||
await _signOut();
|
await handleError(
|
||||||
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
|
emit: emit,
|
||||||
} on AppException catch (e) {
|
action: () async {
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
await _signOut();
|
||||||
emit(
|
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientAuthStatus.error,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientAuthStatus.error,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientAuthStatus.error,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Typed navigation for the Client Authentication feature.
|
|
||||||
///
|
|
||||||
/// This extension on [IModularNavigator] provides named methods for
|
|
||||||
/// navigating between authentication pages, reducing magic strings and
|
|
||||||
/// improving maintainability.
|
|
||||||
extension ClientAuthNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the sign in page using a push named route.
|
|
||||||
void pushClientSignIn() {
|
|
||||||
pushNamed('/client-sign-in');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the sign up page using a push named route.
|
|
||||||
void pushClientSignUp() {
|
|
||||||
pushNamed('/client-sign-up');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the main client home dashboard.
|
|
||||||
///
|
|
||||||
/// Uses absolute path navigation to the client main shell,
|
|
||||||
/// which will display the home tab by default.
|
|
||||||
void navigateClientHome() {
|
|
||||||
navigate('/client-main/home');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import '../navigation/client_auth_navigator.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
class ClientGetStartedPage extends StatelessWidget {
|
class ClientGetStartedPage extends StatelessWidget {
|
||||||
const ClientGetStartedPage({super.key});
|
const ClientGetStartedPage({super.key});
|
||||||
@@ -96,7 +96,7 @@ class ClientGetStartedPage extends StatelessWidget {
|
|||||||
.client_authentication
|
.client_authentication
|
||||||
.get_started_page
|
.get_started_page
|
||||||
.sign_in_button,
|
.sign_in_button,
|
||||||
onPressed: () => Modular.to.pushClientSignIn(),
|
onPressed: () => Modular.to.toClientSignIn(),
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ class ClientGetStartedPage extends StatelessWidget {
|
|||||||
.client_authentication
|
.client_authentication
|
||||||
.get_started_page
|
.get_started_page
|
||||||
.create_account_button,
|
.create_account_button,
|
||||||
onPressed: () => Modular.to.pushClientSignUp(),
|
onPressed: () => Modular.to.toClientSignUp(),
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ 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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../blocs/client_auth_bloc.dart';
|
import '../blocs/client_auth_bloc.dart';
|
||||||
import '../blocs/client_auth_event.dart';
|
import '../blocs/client_auth_event.dart';
|
||||||
import '../blocs/client_auth_state.dart';
|
import '../blocs/client_auth_state.dart';
|
||||||
import '../navigation/client_auth_navigator.dart';
|
|
||||||
import '../widgets/client_sign_in_page/client_sign_in_form.dart';
|
import '../widgets/client_sign_in_page/client_sign_in_form.dart';
|
||||||
import '../widgets/common/auth_divider.dart';
|
import '../widgets/common/auth_divider.dart';
|
||||||
import '../widgets/common/auth_social_button.dart';
|
|
||||||
|
|
||||||
/// Page for client users to sign in to their account.
|
/// Page for client users to sign in to their account.
|
||||||
///
|
///
|
||||||
@@ -38,12 +37,12 @@ class ClientSignInPage extends StatelessWidget {
|
|||||||
final TranslationsClientAuthenticationSignInPageEn i18n = t.client_authentication.sign_in_page;
|
final TranslationsClientAuthenticationSignInPageEn i18n = t.client_authentication.sign_in_page;
|
||||||
final ClientAuthBloc authBloc = Modular.get<ClientAuthBloc>();
|
final ClientAuthBloc authBloc = Modular.get<ClientAuthBloc>();
|
||||||
|
|
||||||
return BlocProvider.value(
|
return BlocProvider<ClientAuthBloc>.value(
|
||||||
value: authBloc,
|
value: authBloc,
|
||||||
child: BlocConsumer<ClientAuthBloc, ClientAuthState>(
|
child: BlocConsumer<ClientAuthBloc, ClientAuthState>(
|
||||||
listener: (BuildContext context, ClientAuthState state) {
|
listener: (BuildContext context, ClientAuthState state) {
|
||||||
if (state.status == ClientAuthStatus.authenticated) {
|
if (state.status == ClientAuthStatus.authenticated) {
|
||||||
Modular.to.navigateClientHome();
|
Modular.to.toClientHome();
|
||||||
} else if (state.status == ClientAuthStatus.error) {
|
} else if (state.status == ClientAuthStatus.error) {
|
||||||
final String errorMessage = state.errorMessage != null
|
final String errorMessage = state.errorMessage != null
|
||||||
? translateErrorKey(state.errorMessage!)
|
? translateErrorKey(state.errorMessage!)
|
||||||
@@ -107,7 +106,7 @@ class ClientSignInPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space1),
|
const SizedBox(width: UiConstants.space1),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => Modular.to.pushClientSignUp(),
|
onTap: () => Modular.to.toClientSignUp(),
|
||||||
child: Text(
|
child: Text(
|
||||||
i18n.sign_up_link,
|
i18n.sign_up_link,
|
||||||
style: UiTypography.body2m.textLink,
|
style: UiTypography.body2m.textLink,
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ 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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../blocs/client_auth_bloc.dart';
|
import '../blocs/client_auth_bloc.dart';
|
||||||
import '../blocs/client_auth_event.dart';
|
import '../blocs/client_auth_event.dart';
|
||||||
import '../blocs/client_auth_state.dart';
|
import '../blocs/client_auth_state.dart';
|
||||||
import '../navigation/client_auth_navigator.dart';
|
|
||||||
import '../widgets/client_sign_up_page/client_sign_up_form.dart';
|
import '../widgets/client_sign_up_page/client_sign_up_form.dart';
|
||||||
import '../widgets/common/auth_divider.dart';
|
import '../widgets/common/auth_divider.dart';
|
||||||
import '../widgets/common/auth_social_button.dart';
|
import '../widgets/common/auth_social_button.dart';
|
||||||
@@ -47,7 +47,7 @@ class ClientSignUpPage extends StatelessWidget {
|
|||||||
child: BlocConsumer<ClientAuthBloc, ClientAuthState>(
|
child: BlocConsumer<ClientAuthBloc, ClientAuthState>(
|
||||||
listener: (BuildContext context, ClientAuthState state) {
|
listener: (BuildContext context, ClientAuthState state) {
|
||||||
if (state.status == ClientAuthStatus.authenticated) {
|
if (state.status == ClientAuthStatus.authenticated) {
|
||||||
Modular.to.navigateClientHome();
|
Modular.to.toClientHome();
|
||||||
} else if (state.status == ClientAuthStatus.error) {
|
} else if (state.status == ClientAuthStatus.error) {
|
||||||
final String errorMessage = state.errorMessage != null
|
final String errorMessage = state.errorMessage != null
|
||||||
? translateErrorKey(state.errorMessage!)
|
? translateErrorKey(state.errorMessage!)
|
||||||
@@ -116,7 +116,7 @@ class ClientSignUpPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space1),
|
const SizedBox(width: UiConstants.space1),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => Modular.to.pushClientSignIn(),
|
onTap: () => Modular.to.toClientSignIn(),
|
||||||
child: Text(
|
child: Text(
|
||||||
i18n.sign_in_link,
|
i18n.sign_in_link,
|
||||||
style: UiTypography.body2m.textLink,
|
style: UiTypography.body2m.textLink,
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ dependencies:
|
|||||||
path: ../../../data_connect
|
path: ../../../data_connect
|
||||||
krow_domain:
|
krow_domain:
|
||||||
path: ../../../domain
|
path: ../../../domain
|
||||||
|
krow_core:
|
||||||
|
path: ../../../core
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/presentation/navigation/billing_navigator.dart';
|
|
||||||
export 'src/billing_module.dart';
|
export 'src/billing_module.dart';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
|
||||||
import 'data/repositories_impl/billing_repository_impl.dart';
|
import 'data/repositories_impl/billing_repository_impl.dart';
|
||||||
@@ -47,6 +48,6 @@ class BillingModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const BillingPage());
|
r.child(ClientPaths.childRoute(ClientPaths.billing, ClientPaths.billing), child: (_) => const BillingPage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] to provide typed navigation for the billing feature.
|
|
||||||
extension BillingNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the billing page.
|
|
||||||
void pushBilling() => pushNamed('/billing/');
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ 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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../blocs/billing_bloc.dart';
|
import '../blocs/billing_bloc.dart';
|
||||||
import '../blocs/billing_event.dart';
|
import '../blocs/billing_event.dart';
|
||||||
@@ -83,7 +84,7 @@ class _BillingViewState extends State<BillingView> {
|
|||||||
leading: Center(
|
leading: Center(
|
||||||
child: UiIconButton.secondary(
|
child: UiIconButton.secondary(
|
||||||
icon: UiIcons.arrowLeft,
|
icon: UiIcons.arrowLeft,
|
||||||
onTap: () => Modular.to.navigate('/client-main/home/'),
|
onTap: () => Modular.to.toClientHome()
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: AnimatedSwitcher(
|
title: AnimatedSwitcher(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'data/repositories_impl/coverage_repository_impl.dart';
|
import 'data/repositories_impl/coverage_repository_impl.dart';
|
||||||
import 'domain/repositories/coverage_repository.dart';
|
import 'domain/repositories/coverage_repository.dart';
|
||||||
@@ -31,6 +32,7 @@ class CoverageModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const CoveragePage());
|
r.child(ClientPaths.childRoute(ClientPaths.coverage, ClientPaths.coverage),
|
||||||
|
child: (_) => const CoveragePage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import '../blocs/coverage_bloc.dart';
|
import '../blocs/coverage_bloc.dart';
|
||||||
import '../blocs/coverage_event.dart';
|
import '../blocs/coverage_event.dart';
|
||||||
import '../blocs/coverage_state.dart';
|
import '../blocs/coverage_state.dart';
|
||||||
@@ -68,7 +69,7 @@ class _CoveragePageState extends State<CoveragePage> {
|
|||||||
expandedHeight: 300.0,
|
expandedHeight: 300.0,
|
||||||
backgroundColor: UiColors.primary,
|
backgroundColor: UiColors.primary,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () => Modular.to.navigate('/client-main/home/'),
|
onPressed: () => Modular.to.toClientHome(),
|
||||||
icon: Container(
|
icon: Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space2),
|
padding: const EdgeInsets.all(UiConstants.space2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'coverage_calendar_selector.dart';
|
import 'coverage_calendar_selector.dart';
|
||||||
|
|
||||||
/// Header widget for the coverage page.
|
/// Header widget for the coverage page.
|
||||||
@@ -67,7 +68,7 @@ class CoverageHeader extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => Modular.to.navigate('/client-main/home/'),
|
onTap: () => Modular.to.toClientHome(),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: UiConstants.space10,
|
width: UiConstants.space10,
|
||||||
height: UiConstants.space10,
|
height: UiConstants.space10,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
export 'src/client_main_module.dart';
|
export 'src/client_main_module.dart';
|
||||||
export 'src/presentation/navigation/client_main_navigator.dart';
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:client_home/client_home.dart';
|
|||||||
import 'package:client_coverage/client_coverage.dart';
|
import 'package:client_coverage/client_coverage.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:view_orders/view_orders.dart';
|
import 'package:view_orders/view_orders.dart';
|
||||||
|
|
||||||
import 'presentation/blocs/client_main_cubit.dart';
|
import 'presentation/blocs/client_main_cubit.dart';
|
||||||
@@ -21,12 +22,24 @@ class ClientMainModule extends Module {
|
|||||||
'/',
|
'/',
|
||||||
child: (BuildContext context) => const ClientMainPage(),
|
child: (BuildContext context) => const ClientMainPage(),
|
||||||
children: <ParallelRoute<dynamic>>[
|
children: <ParallelRoute<dynamic>>[
|
||||||
ModuleRoute<dynamic>('/home', module: ClientHomeModule()),
|
ModuleRoute<dynamic>(
|
||||||
ModuleRoute<dynamic>('/coverage', module: CoverageModule()),
|
ClientPaths.childRoute(ClientPaths.main, ClientPaths.home),
|
||||||
ModuleRoute<dynamic>('/billing', module: BillingModule()),
|
module: ClientHomeModule(),
|
||||||
ModuleRoute<dynamic>('/orders', module: ViewOrdersModule()),
|
),
|
||||||
|
ModuleRoute<dynamic>(
|
||||||
|
ClientPaths.childRoute(ClientPaths.main, ClientPaths.coverage),
|
||||||
|
module: CoverageModule(),
|
||||||
|
),
|
||||||
|
ModuleRoute<dynamic>(
|
||||||
|
ClientPaths.childRoute(ClientPaths.main, ClientPaths.billing),
|
||||||
|
module: BillingModule(),
|
||||||
|
),
|
||||||
|
ModuleRoute<dynamic>(
|
||||||
|
ClientPaths.childRoute(ClientPaths.main, ClientPaths.orders),
|
||||||
|
module: ViewOrdersModule(),
|
||||||
|
),
|
||||||
ChildRoute<dynamic>(
|
ChildRoute<dynamic>(
|
||||||
'/reports',
|
ClientPaths.childRoute(ClientPaths.main, ClientPaths.reports),
|
||||||
child: (BuildContext context) =>
|
child: (BuildContext context) =>
|
||||||
const PlaceholderPage(title: 'Reports'),
|
const PlaceholderPage(title: 'Reports'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'client_main_state.dart';
|
import 'client_main_state.dart';
|
||||||
|
|
||||||
class ClientMainCubit extends Cubit<ClientMainState> implements Disposable {
|
class ClientMainCubit extends Cubit<ClientMainState> implements Disposable {
|
||||||
@@ -14,15 +15,15 @@ class ClientMainCubit extends Cubit<ClientMainState> implements Disposable {
|
|||||||
|
|
||||||
// Detect which tab is active based on the route path
|
// Detect which tab is active based on the route path
|
||||||
// Using contains() to handle child routes and trailing slashes
|
// Using contains() to handle child routes and trailing slashes
|
||||||
if (path.contains('/client-main/coverage')) {
|
if (path.contains(ClientPaths.coverage)) {
|
||||||
newIndex = 0;
|
newIndex = 0;
|
||||||
} else if (path.contains('/client-main/billing')) {
|
} else if (path.contains(ClientPaths.billing)) {
|
||||||
newIndex = 1;
|
newIndex = 1;
|
||||||
} else if (path.contains('/client-main/home')) {
|
} else if (path.contains(ClientPaths.home)) {
|
||||||
newIndex = 2;
|
newIndex = 2;
|
||||||
} else if (path.contains('/client-main/orders')) {
|
} else if (path.contains(ClientPaths.orders)) {
|
||||||
newIndex = 3;
|
newIndex = 3;
|
||||||
} else if (path.contains('/client-main/reports')) {
|
} else if (path.contains(ClientPaths.reports)) {
|
||||||
newIndex = 4;
|
newIndex = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,19 +37,19 @@ class ClientMainCubit extends Cubit<ClientMainState> implements Disposable {
|
|||||||
|
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
Modular.to.navigate('/client-main/coverage');
|
Modular.to.navigate(ClientPaths.coverage);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
Modular.to.navigate('/client-main/billing');
|
Modular.to.navigate(ClientPaths.billing);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
Modular.to.navigate('/client-main/home');
|
Modular.to.navigate(ClientPaths.home);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
Modular.to.navigate('/client-main/orders');
|
Modular.to.navigate(ClientPaths.orders);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
Modular.to.navigate('/client-main/reports');
|
Modular.to.navigate(ClientPaths.reports);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// State update will happen via _onRouteChanged
|
// State update will happen via _onRouteChanged
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Extension to provide typed navigation for the Client Main feature.
|
|
||||||
extension ClientMainNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the Client Main Shell (Home).
|
|
||||||
/// This replaces the current navigation stack.
|
|
||||||
void navigateClientMain() {
|
|
||||||
navigate('/client-main/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||||
import 'data/repositories_impl/client_create_order_repository_impl.dart';
|
import 'data/repositories_impl/client_create_order_repository_impl.dart';
|
||||||
@@ -57,17 +58,20 @@ class ClientCreateOrderModule extends Module {
|
|||||||
'/',
|
'/',
|
||||||
child: (BuildContext context) => const ClientCreateOrderPage(),
|
child: (BuildContext context) => const ClientCreateOrderPage(),
|
||||||
);
|
);
|
||||||
r.child('/rapid', child: (BuildContext context) => const RapidOrderPage());
|
|
||||||
r.child(
|
r.child(
|
||||||
'/one-time',
|
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRapid),
|
||||||
|
child: (BuildContext context) => const RapidOrderPage(),
|
||||||
|
);
|
||||||
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderOneTime),
|
||||||
child: (BuildContext context) => const OneTimeOrderPage(),
|
child: (BuildContext context) => const OneTimeOrderPage(),
|
||||||
);
|
);
|
||||||
r.child(
|
r.child(
|
||||||
'/recurring',
|
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRecurring),
|
||||||
child: (BuildContext context) => const RecurringOrderPage(),
|
child: (BuildContext context) => const RecurringOrderPage(),
|
||||||
);
|
);
|
||||||
r.child(
|
r.child(
|
||||||
'/permanent',
|
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderPermanent),
|
||||||
child: (BuildContext context) => const PermanentOrderPage(),
|
child: (BuildContext context) => const PermanentOrderPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
extension ClientCreateOrderNavigator on IModularNavigator {
|
|
||||||
void pushCreateOrder() {
|
|
||||||
pushNamed('/client/create-order/');
|
|
||||||
}
|
|
||||||
|
|
||||||
void pushRapidOrder() {
|
|
||||||
pushNamed('/client/create-order/rapid');
|
|
||||||
}
|
|
||||||
|
|
||||||
void pushOneTimeOrder() {
|
|
||||||
pushNamed('/client/create-order/one-time');
|
|
||||||
}
|
|
||||||
|
|
||||||
void pushRecurringOrder() {
|
|
||||||
pushNamed('/client/create-order/recurring');
|
|
||||||
}
|
|
||||||
|
|
||||||
void pushPermanentOrder() {
|
|
||||||
pushNamed('/client/create-order/permanent');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
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_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
/// Permanent Order Page - Long-term staffing placement.
|
/// Permanent Order Page - Long-term staffing placement.
|
||||||
/// Placeholder for future implementation.
|
/// Placeholder for future implementation.
|
||||||
@@ -15,10 +16,9 @@ class PermanentOrderPage extends StatelessWidget {
|
|||||||
t.client_create_order.permanent;
|
t.client_create_order.permanent;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
|
||||||
appBar: UiAppBar(
|
appBar: UiAppBar(
|
||||||
title: labels.title,
|
title: labels.title,
|
||||||
onLeadingPressed: () => Modular.to.navigate('/client/create-order/'),
|
onLeadingPressed: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
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_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
/// Recurring Order Page - Ongoing weekly/monthly coverage.
|
/// Recurring Order Page - Ongoing weekly/monthly coverage.
|
||||||
/// Placeholder for future implementation.
|
/// Placeholder for future implementation.
|
||||||
@@ -15,10 +16,9 @@ class RecurringOrderPage extends StatelessWidget {
|
|||||||
t.client_create_order.recurring;
|
t.client_create_order.recurring;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
|
||||||
appBar: UiAppBar(
|
appBar: UiAppBar(
|
||||||
title: labels.title,
|
title: labels.title,
|
||||||
onLeadingPressed: () => Modular.to.navigate('/client/create-order/'),
|
onLeadingPressed: () => Modular.to.toClientHome(),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ 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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../blocs/client_create_order_bloc.dart';
|
import '../../blocs/client_create_order_bloc.dart';
|
||||||
import '../../blocs/client_create_order_state.dart';
|
import '../../blocs/client_create_order_state.dart';
|
||||||
import '../../navigation/client_create_order_navigator.dart';
|
|
||||||
import '../../ui_entities/order_type_ui_metadata.dart';
|
import '../../ui_entities/order_type_ui_metadata.dart';
|
||||||
import '../order_type_card.dart';
|
import '../order_type_card.dart';
|
||||||
|
|
||||||
@@ -40,10 +40,9 @@ class CreateOrderView extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
|
||||||
appBar: UiAppBar(
|
appBar: UiAppBar(
|
||||||
title: t.client_create_order.title,
|
title: t.client_create_order.title,
|
||||||
onLeadingPressed: () => Modular.to.navigate('/client-main/home/'),
|
onLeadingPressed: () => Modular.to.toClientHome(),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -98,16 +97,16 @@ class CreateOrderView extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
switch (type.id) {
|
switch (type.id) {
|
||||||
case 'rapid':
|
case 'rapid':
|
||||||
Modular.to.pushRapidOrder();
|
Modular.to.toCreateOrderRapid();
|
||||||
break;
|
break;
|
||||||
case 'one-time':
|
case 'one-time':
|
||||||
Modular.to.pushOneTimeOrder();
|
Modular.to.toCreateOrderOneTime();
|
||||||
break;
|
break;
|
||||||
case 'recurring':
|
case 'recurring':
|
||||||
Modular.to.pushRecurringOrder();
|
Modular.to.toCreateOrderRecurring();
|
||||||
break;
|
break;
|
||||||
case 'permanent':
|
case 'permanent':
|
||||||
Modular.to.pushPermanentOrder();
|
Modular.to.toCreateOrderPermanent();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../blocs/one_time_order_bloc.dart';
|
import '../../blocs/one_time_order_bloc.dart';
|
||||||
import '../../blocs/one_time_order_event.dart';
|
import '../../blocs/one_time_order_event.dart';
|
||||||
@@ -32,7 +33,7 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
message: labels.success_message,
|
message: labels.success_message,
|
||||||
buttonLabel: labels.back_to_orders,
|
buttonLabel: labels.back_to_orders,
|
||||||
onDone: () => Modular.to.pushNamedAndRemoveUntil(
|
onDone: () => Modular.to.pushNamedAndRemoveUntil(
|
||||||
'/client-main/orders/',
|
ClientPaths.orders,
|
||||||
(_) => false,
|
(_) => false,
|
||||||
arguments: <String, dynamic>{
|
arguments: <String, dynamic>{
|
||||||
'initialDate': state.date.toIso8601String(),
|
'initialDate': state.date.toIso8601String(),
|
||||||
@@ -44,13 +45,12 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
if (state.vendors.isEmpty &&
|
if (state.vendors.isEmpty &&
|
||||||
state.status != OneTimeOrderStatus.loading) {
|
state.status != OneTimeOrderStatus.loading) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
OneTimeOrderHeader(
|
OneTimeOrderHeader(
|
||||||
title: labels.title,
|
title: labels.title,
|
||||||
subtitle: labels.subtitle,
|
subtitle: labels.subtitle,
|
||||||
onBack: () => Modular.to.navigate('/client/create-order/'),
|
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -83,13 +83,12 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
OneTimeOrderHeader(
|
OneTimeOrderHeader(
|
||||||
title: labels.title,
|
title: labels.title,
|
||||||
subtitle: labels.subtitle,
|
subtitle: labels.subtitle,
|
||||||
onBack: () => Modular.to.navigate('/client/create-order/'),
|
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import '../../blocs/rapid_order_bloc.dart';
|
import '../../blocs/rapid_order_bloc.dart';
|
||||||
import '../../blocs/rapid_order_event.dart';
|
import '../../blocs/rapid_order_event.dart';
|
||||||
import '../../blocs/rapid_order_state.dart';
|
import '../../blocs/rapid_order_state.dart';
|
||||||
@@ -28,7 +29,7 @@ class RapidOrderView extends StatelessWidget {
|
|||||||
title: labels.success_title,
|
title: labels.success_title,
|
||||||
message: labels.success_message,
|
message: labels.success_message,
|
||||||
buttonLabel: labels.back_to_orders,
|
buttonLabel: labels.back_to_orders,
|
||||||
onDone: () => Modular.to.navigate('/client-main/orders/'),
|
onDone: () => Modular.to.navigate(ClientPaths.orders),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,6 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
|
||||||
body: Column(
|
body: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
RapidOrderHeader(
|
RapidOrderHeader(
|
||||||
@@ -82,7 +82,7 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
subtitle: labels.subtitle,
|
subtitle: labels.subtitle,
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
time: timeStr,
|
time: timeStr,
|
||||||
onBack: () => Modular.to.navigate('/client/create-order/'),
|
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
|
||||||
import 'src/data/repositories_impl/home_repository_impl.dart';
|
import 'src/data/repositories_impl/home_repository_impl.dart';
|
||||||
import 'src/domain/repositories/home_repository_interface.dart';
|
import 'src/domain/repositories/home_repository_interface.dart';
|
||||||
import 'src/domain/usecases/get_dashboard_data_usecase.dart';
|
import 'src/domain/usecases/get_dashboard_data_usecase.dart';
|
||||||
@@ -9,7 +11,6 @@ import 'src/presentation/blocs/client_home_bloc.dart';
|
|||||||
import 'src/presentation/pages/client_home_page.dart';
|
import 'src/presentation/pages/client_home_page.dart';
|
||||||
|
|
||||||
export 'src/presentation/pages/client_home_page.dart';
|
export 'src/presentation/pages/client_home_page.dart';
|
||||||
export 'src/presentation/navigation/client_home_navigator.dart';
|
|
||||||
|
|
||||||
/// A [Module] for the client home feature.
|
/// A [Module] for the client home feature.
|
||||||
///
|
///
|
||||||
@@ -46,6 +47,9 @@ class ClientHomeModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const ClientHomePage());
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.home, ClientPaths.home),
|
||||||
|
child: (_) => const ClientHomePage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ 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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import '../blocs/client_home_bloc.dart';
|
import '../blocs/client_home_bloc.dart';
|
||||||
import '../blocs/client_home_event.dart';
|
import '../blocs/client_home_event.dart';
|
||||||
import '../blocs/client_home_state.dart';
|
import '../blocs/client_home_state.dart';
|
||||||
import '../navigation/client_home_navigator.dart';
|
|
||||||
import 'header_icon_button.dart';
|
import 'header_icon_button.dart';
|
||||||
|
|
||||||
/// The header section of the client home page.
|
/// The header section of the client home page.
|
||||||
@@ -95,7 +95,7 @@ class ClientHomeHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
HeaderIconButton(
|
HeaderIconButton(
|
||||||
icon: UiIcons.settings,
|
icon: UiIcons.settings,
|
||||||
onTap: () => Modular.to.pushSettings(),
|
onTap: () => Modular.to.toClientSettings(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'shift_order_form_sheet.dart';
|
||||||
import '../widgets/shift_order_form_sheet.dart';
|
|
||||||
|
|
||||||
extension ClientHomeNavigator on IModularNavigator {
|
|
||||||
void pushSettings() {
|
|
||||||
pushNamed('/client-settings/');
|
|
||||||
}
|
|
||||||
|
|
||||||
void pushCreateOrder() {
|
|
||||||
pushNamed('/client/create-order/');
|
|
||||||
}
|
|
||||||
|
|
||||||
void pushRapidOrder() {
|
|
||||||
pushNamed('/client/create-order/rapid');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper class for showing modal sheets in the client home feature.
|
/// Helper class for showing modal sheets in the client home feature.
|
||||||
class ClientHomeSheets {
|
class ClientHomeSheets {
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import '../blocs/client_home_state.dart';
|
import '../blocs/client_home_state.dart';
|
||||||
import '../navigation/client_home_navigator.dart';
|
|
||||||
import '../widgets/actions_widget.dart';
|
import '../widgets/actions_widget.dart';
|
||||||
import '../widgets/coverage_widget.dart';
|
import '../widgets/coverage_widget.dart';
|
||||||
import '../widgets/draggable_widget_wrapper.dart';
|
import '../widgets/draggable_widget_wrapper.dart';
|
||||||
import '../widgets/live_activity_widget.dart';
|
import '../widgets/live_activity_widget.dart';
|
||||||
import '../widgets/reorder_widget.dart';
|
import '../widgets/reorder_widget.dart';
|
||||||
import '../widgets/spending_widget.dart';
|
import '../widgets/spending_widget.dart';
|
||||||
|
import 'client_home_sheets.dart';
|
||||||
|
|
||||||
/// A widget that builds dashboard content based on widget ID.
|
/// A widget that builds dashboard content based on widget ID.
|
||||||
///
|
///
|
||||||
@@ -62,8 +63,8 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
|||||||
switch (id) {
|
switch (id) {
|
||||||
case 'actions':
|
case 'actions':
|
||||||
return ActionsWidget(
|
return ActionsWidget(
|
||||||
onRapidPressed: () => Modular.to.pushRapidOrder(),
|
onRapidPressed: () => Modular.to.toCreateOrderRapid(),
|
||||||
onCreateOrderPressed: () => Modular.to.pushCreateOrder(),
|
onCreateOrderPressed: () => Modular.to.toCreateOrder(),
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
);
|
);
|
||||||
case 'reorder':
|
case 'reorder':
|
||||||
@@ -116,7 +117,7 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
case 'liveActivity':
|
case 'liveActivity':
|
||||||
return LiveActivityWidget(
|
return LiveActivityWidget(
|
||||||
onViewAllPressed: () => Modular.to.navigate('/client-main/coverage/'),
|
onViewAllPressed: () => Modular.to.toClientCoverage(),
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
library client_hubs;
|
library client_hubs;
|
||||||
|
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||||
import 'src/data/repositories_impl/hub_repository_impl.dart';
|
import 'src/data/repositories_impl/hub_repository_impl.dart';
|
||||||
@@ -48,6 +49,6 @@ class ClientHubsModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const ClientHubsPage());
|
r.child(ClientPaths.childRoute(ClientPaths.hubs, ClientPaths.hubs), child: (_) => const ClientHubsPage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
import 'package:http/http.dart' as http;
|
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_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||||
import 'package:krow_domain/krow_domain.dart'
|
import 'package:krow_domain/krow_domain.dart'
|
||||||
@@ -262,7 +263,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
<String, String>{
|
<String, String>{
|
||||||
'place_id': placeId,
|
'place_id': placeId,
|
||||||
'fields': 'address_component',
|
'fields': 'address_component',
|
||||||
'key': HubsConstants.googlePlacesApiKey,
|
'key': AppConfig.googlePlacesApiKey,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:developer' as developer;
|
|
||||||
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../domain/arguments/assign_nfc_tag_arguments.dart';
|
import '../../domain/arguments/assign_nfc_tag_arguments.dart';
|
||||||
import '../../domain/arguments/create_hub_arguments.dart';
|
import '../../domain/arguments/create_hub_arguments.dart';
|
||||||
@@ -18,6 +17,7 @@ import 'client_hubs_state.dart';
|
|||||||
/// It orchestrates the flow between the UI and the domain layer by invoking
|
/// It orchestrates the flow between the UI and the domain layer by invoking
|
||||||
/// specific use cases for fetching, creating, deleting, and assigning tags to hubs.
|
/// specific use cases for fetching, creating, deleting, and assigning tags to hubs.
|
||||||
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
||||||
|
with BlocErrorHandler<ClientHubsState>
|
||||||
implements Disposable {
|
implements Disposable {
|
||||||
final GetHubsUseCase _getHubsUseCase;
|
final GetHubsUseCase _getHubsUseCase;
|
||||||
final CreateHubUseCase _createHubUseCase;
|
final CreateHubUseCase _createHubUseCase;
|
||||||
@@ -66,26 +66,18 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
Emitter<ClientHubsState> emit,
|
Emitter<ClientHubsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientHubsStatus.loading));
|
emit(state.copyWith(status: ClientHubsStatus.loading));
|
||||||
try {
|
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
await handleError(
|
||||||
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
|
emit: emit,
|
||||||
} on AppException catch (e) {
|
action: () async {
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
|
final hubs = await _getHubsUseCase();
|
||||||
emit(
|
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientHubsStatus.failure,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientHubsStatus.failure,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.failure,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAddRequested(
|
Future<void> _onAddRequested(
|
||||||
@@ -93,47 +85,39 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
Emitter<ClientHubsState> emit,
|
Emitter<ClientHubsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
||||||
try {
|
|
||||||
await _createHubUseCase(
|
await handleError(
|
||||||
CreateHubArguments(
|
emit: emit,
|
||||||
name: event.name,
|
action: () async {
|
||||||
address: event.address,
|
await _createHubUseCase(
|
||||||
placeId: event.placeId,
|
CreateHubArguments(
|
||||||
latitude: event.latitude,
|
name: event.name,
|
||||||
longitude: event.longitude,
|
address: event.address,
|
||||||
city: event.city,
|
placeId: event.placeId,
|
||||||
state: event.state,
|
latitude: event.latitude,
|
||||||
street: event.street,
|
longitude: event.longitude,
|
||||||
country: event.country,
|
city: event.city,
|
||||||
zipCode: event.zipCode,
|
state: event.state,
|
||||||
),
|
street: event.street,
|
||||||
);
|
country: event.country,
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
zipCode: event.zipCode,
|
||||||
emit(
|
),
|
||||||
state.copyWith(
|
);
|
||||||
status: ClientHubsStatus.actionSuccess,
|
final hubs = await _getHubsUseCase();
|
||||||
hubs: hubs,
|
emit(
|
||||||
successMessage: 'Hub created successfully',
|
state.copyWith(
|
||||||
showAddHubDialog: false,
|
status: ClientHubsStatus.actionSuccess,
|
||||||
),
|
hubs: hubs,
|
||||||
);
|
successMessage: 'Hub created successfully',
|
||||||
} on AppException catch (e) {
|
showAddHubDialog: false,
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
|
),
|
||||||
emit(
|
);
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientHubsStatus.actionFailure,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientHubsStatus.actionFailure,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDeleteRequested(
|
Future<void> _onDeleteRequested(
|
||||||
@@ -141,33 +125,25 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
Emitter<ClientHubsState> emit,
|
Emitter<ClientHubsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
||||||
try {
|
|
||||||
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
|
await handleError(
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
emit: emit,
|
||||||
emit(
|
action: () async {
|
||||||
state.copyWith(
|
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
|
||||||
status: ClientHubsStatus.actionSuccess,
|
final hubs = await _getHubsUseCase();
|
||||||
hubs: hubs,
|
emit(
|
||||||
successMessage: 'Hub deleted successfully',
|
state.copyWith(
|
||||||
),
|
status: ClientHubsStatus.actionSuccess,
|
||||||
);
|
hubs: hubs,
|
||||||
} on AppException catch (e) {
|
successMessage: 'Hub deleted successfully',
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
|
),
|
||||||
emit(
|
);
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientHubsStatus.actionFailure,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientHubsStatus.actionFailure,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onNfcTagAssignRequested(
|
Future<void> _onNfcTagAssignRequested(
|
||||||
@@ -175,36 +151,28 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
Emitter<ClientHubsState> emit,
|
Emitter<ClientHubsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
||||||
try {
|
|
||||||
await _assignNfcTagUseCase(
|
await handleError(
|
||||||
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
|
emit: emit,
|
||||||
);
|
action: () async {
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
await _assignNfcTagUseCase(
|
||||||
emit(
|
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
|
||||||
state.copyWith(
|
);
|
||||||
status: ClientHubsStatus.actionSuccess,
|
final hubs = await _getHubsUseCase();
|
||||||
hubs: hubs,
|
emit(
|
||||||
successMessage: 'NFC tag assigned successfully',
|
state.copyWith(
|
||||||
clearHubToIdentify: true,
|
status: ClientHubsStatus.actionSuccess,
|
||||||
),
|
hubs: hubs,
|
||||||
);
|
successMessage: 'NFC tag assigned successfully',
|
||||||
} on AppException catch (e) {
|
clearHubToIdentify: true,
|
||||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
|
),
|
||||||
emit(
|
);
|
||||||
state.copyWith(
|
},
|
||||||
status: ClientHubsStatus.actionFailure,
|
onError: (errorKey) => state.copyWith(
|
||||||
errorMessage: e.messageKey,
|
status: ClientHubsStatus.actionFailure,
|
||||||
),
|
errorMessage: errorKey,
|
||||||
);
|
),
|
||||||
} catch (e) {
|
);
|
||||||
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: ClientHubsStatus.actionFailure,
|
|
||||||
errorMessage: 'errors.generic.unknown',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onMessageCleared(
|
void _onMessageCleared(
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] to provide typed navigation for client hubs.
|
|
||||||
extension ClientHubsNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the client hubs page.
|
|
||||||
Future<void> pushClientHubs() async {
|
|
||||||
await pushNamed('/client-hubs/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../blocs/client_hubs_bloc.dart';
|
import '../blocs/client_hubs_bloc.dart';
|
||||||
import '../blocs/client_hubs_event.dart';
|
import '../blocs/client_hubs_event.dart';
|
||||||
@@ -179,7 +180,7 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => Modular.to.navigate('/client-main/home/'),
|
onTap: () => Modular.to.toClientHome(),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_places_flutter/google_places_flutter.dart';
|
import 'package:google_places_flutter/google_places_flutter.dart';
|
||||||
import 'package:google_places_flutter/model/prediction.dart';
|
import 'package:google_places_flutter/model/prediction.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../../util/hubs_constants.dart';
|
import '../../util/hubs_constants.dart';
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ class HubAddressAutocomplete extends StatelessWidget {
|
|||||||
return GooglePlaceAutoCompleteTextField(
|
return GooglePlaceAutoCompleteTextField(
|
||||||
textEditingController: controller,
|
textEditingController: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
googleAPIKey: HubsConstants.googlePlacesApiKey,
|
googleAPIKey: AppConfig.googlePlacesApiKey,
|
||||||
debounceTime: 500,
|
debounceTime: 500,
|
||||||
countries: HubsConstants.supportedCountries,
|
countries: HubsConstants.supportedCountries,
|
||||||
isLatLngRequired: true,
|
isLatLngRequired: true,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
class HubsConstants {
|
class HubsConstants {
|
||||||
static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY');
|
|
||||||
static const List<String> supportedCountries = <String>['us'];
|
static const List<String> supportedCountries = <String>['us'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'src/data/repositories_impl/settings_repository_impl.dart';
|
import 'src/data/repositories_impl/settings_repository_impl.dart';
|
||||||
import 'src/domain/repositories/settings_repository_interface.dart';
|
import 'src/domain/repositories/settings_repository_interface.dart';
|
||||||
import 'src/domain/usecases/sign_out_usecase.dart';
|
import 'src/domain/usecases/sign_out_usecase.dart';
|
||||||
@@ -26,6 +27,9 @@ class ClientSettingsModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const ClientSettingsPage());
|
r.child(
|
||||||
|
ClientPaths.childRoute(ClientPaths.settings, ClientPaths.settings),
|
||||||
|
child: (_) => const ClientSettingsPage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] to provide strongly-typed navigation
|
|
||||||
/// for the client settings feature.
|
|
||||||
extension ClientSettingsNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the client settings page.
|
|
||||||
void pushClientSettings() {
|
|
||||||
pushNamed('/client/settings/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the hubs page.
|
|
||||||
void pushHubs() {
|
|
||||||
pushNamed('/client-hubs/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
import '../blocs/client_settings_bloc.dart';
|
import '../blocs/client_settings_bloc.dart';
|
||||||
import '../widgets/client_settings_page/settings_actions.dart';
|
import '../widgets/client_settings_page/settings_actions.dart';
|
||||||
@@ -26,7 +27,7 @@ class ClientSettingsPage extends StatelessWidget {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Signed out successfully')),
|
const SnackBar(content: Text('Signed out successfully')),
|
||||||
);
|
);
|
||||||
Modular.to.navigate('/');
|
Modular.to.toClientRoot();
|
||||||
}
|
}
|
||||||
if (state is ClientSettingsError) {
|
if (state is ClientSettingsError) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:client_settings/src/presentation/navigation/client_settings_navigator.dart';
|
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import '../../blocs/client_settings_bloc.dart';
|
import '../../blocs/client_settings_bloc.dart';
|
||||||
|
|
||||||
/// A widget that displays the primary actions for the settings page.
|
/// A widget that displays the primary actions for the settings page.
|
||||||
@@ -30,7 +30,7 @@ class SettingsActions extends StatelessWidget {
|
|||||||
// Hubs button
|
// Hubs button
|
||||||
UiButton.primary(
|
UiButton.primary(
|
||||||
text: labels.hubs,
|
text: labels.hubs,
|
||||||
onPressed: () => Modular.to.pushHubs(),
|
onPressed: () => Modular.to.toClientHubs(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
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_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
|
|
||||||
/// A widget that displays the profile header with avatar and company info.
|
/// A widget that displays the profile header with avatar and company info.
|
||||||
@@ -30,7 +31,7 @@ class SettingsProfileHeader extends StatelessWidget {
|
|||||||
shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)),
|
shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
|
||||||
onPressed: () => Modular.to.navigate('/client-main/home/'),
|
onPressed: () => Modular.to.toClientHome(),
|
||||||
),
|
),
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
background: Container(
|
background: Container(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
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_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import '../../navigation/client_settings_navigator.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
/// A widget that displays a list of quick links in a card.
|
/// A widget that displays a list of quick links in a card.
|
||||||
class SettingsQuickLinks extends StatelessWidget {
|
class SettingsQuickLinks extends StatelessWidget {
|
||||||
@@ -37,7 +37,7 @@ class SettingsQuickLinks extends StatelessWidget {
|
|||||||
_QuickLinkItem(
|
_QuickLinkItem(
|
||||||
icon: UiIcons.nfc,
|
icon: UiIcons.nfc,
|
||||||
title: labels.clock_in_hubs,
|
title: labels.clock_in_hubs,
|
||||||
onTap: () => Modular.to.pushHubs(),
|
onTap: () => Modular.to.toClientHubs(),
|
||||||
),
|
),
|
||||||
|
|
||||||
_QuickLinkItem(
|
_QuickLinkItem(
|
||||||
|
|||||||
@@ -212,9 +212,6 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
|
|||||||
final List<OrderItem> ordersOnDate = state.orders
|
final List<OrderItem> ordersOnDate = state.orders
|
||||||
.where((OrderItem s) => s.date == selectedDateStr)
|
.where((OrderItem s) => s.date == selectedDateStr)
|
||||||
.toList();
|
.toList();
|
||||||
print(
|
|
||||||
'ViewOrders selectedDate=$selectedDateStr ordersOnDate=${ordersOnDate.length}',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort by start time
|
// Sort by start time
|
||||||
ordersOnDate.sort(
|
ordersOnDate.sort(
|
||||||
@@ -256,20 +253,18 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
|
|||||||
|
|
||||||
int _calculateCategoryCount(String category) {
|
int _calculateCategoryCount(String category) {
|
||||||
if (state.selectedDate == null) return 0;
|
if (state.selectedDate == null) return 0;
|
||||||
|
|
||||||
final String selectedDateStr = DateFormat(
|
final String selectedDateStr = DateFormat(
|
||||||
'yyyy-MM-dd',
|
'yyyy-MM-dd',
|
||||||
).format(state.selectedDate!);
|
).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.date == selectedDateStr && 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.date == selectedDateStr && s.status == 'COMPLETED')
|
||||||
.length;
|
.length;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -277,16 +272,15 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
|
|||||||
|
|
||||||
int _calculateUpNextCount() {
|
int _calculateUpNextCount() {
|
||||||
if (state.selectedDate == null) return 0;
|
if (state.selectedDate == null) return 0;
|
||||||
|
|
||||||
final String selectedDateStr = DateFormat(
|
final String selectedDateStr = DateFormat(
|
||||||
'yyyy-MM-dd',
|
'yyyy-MM-dd',
|
||||||
).format(state.selectedDate!);
|
).format(state.selectedDate!);
|
||||||
final List<OrderItem> ordersOnDate = state.orders
|
|
||||||
.where((OrderItem s) => s.date == selectedDateStr)
|
return state.orders
|
||||||
.toList();
|
|
||||||
return ordersOnDate
|
|
||||||
.where(
|
.where(
|
||||||
(OrderItem s) =>
|
(OrderItem s) =>
|
||||||
// TODO(orders): move PENDING to its own tab once available.
|
s.date == selectedDateStr &&
|
||||||
<String>['OPEN', 'FILLED', 'CONFIRMED', 'PENDING', 'ASSIGNED']
|
<String>['OPEN', 'FILLED', 'CONFIRMED', 'PENDING', 'ASSIGNED']
|
||||||
.contains(s.status),
|
.contains(s.status),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Extension to provide typed navigation for the View Orders feature.
|
|
||||||
extension ViewOrdersNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the Create Order feature.
|
|
||||||
void navigateToCreateOrder() {
|
|
||||||
navigate('/client/create-order/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the Order Details (placeholder for now).
|
|
||||||
void navigateToOrderDetails(String orderId) {
|
|
||||||
// pushNamed('/view-orders/$orderId');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,12 @@ import 'package:flutter_modular/flutter_modular.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import '../blocs/view_orders_cubit.dart';
|
import '../blocs/view_orders_cubit.dart';
|
||||||
import '../blocs/view_orders_state.dart';
|
import '../blocs/view_orders_state.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../widgets/view_order_card.dart';
|
import '../widgets/view_order_card.dart';
|
||||||
import '../widgets/view_orders_header.dart';
|
import '../widgets/view_orders_header.dart';
|
||||||
import '../navigation/view_orders_navigator.dart';
|
|
||||||
|
|
||||||
/// The main page for viewing client orders.
|
/// The main page for viewing client orders.
|
||||||
///
|
///
|
||||||
@@ -191,7 +191,7 @@ class _ViewOrdersViewState extends State<ViewOrdersView> {
|
|||||||
UiButton.primary(
|
UiButton.primary(
|
||||||
text: t.client_view_orders.post_order,
|
text: t.client_view_orders.post_order,
|
||||||
leadingIcon: UiIcons.add,
|
leadingIcon: UiIcons.add,
|
||||||
onPressed: () => Modular.to.navigateToCreateOrder(),
|
onPressed: () => Modular.to.toCreateOrder(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../blocs/view_orders_cubit.dart';
|
import '../blocs/view_orders_cubit.dart';
|
||||||
import '../blocs/view_orders_state.dart';
|
import '../blocs/view_orders_state.dart';
|
||||||
import '../navigation/view_orders_navigator.dart';
|
|
||||||
import 'view_orders_filter_tab.dart';
|
import 'view_orders_filter_tab.dart';
|
||||||
|
|
||||||
/// The sticky header section for the View Orders page.
|
/// The sticky header section for the View Orders page.
|
||||||
@@ -69,7 +69,7 @@ class ViewOrdersHeader extends StatelessWidget {
|
|||||||
UiButton.primary(
|
UiButton.primary(
|
||||||
text: t.client_view_orders.post_button,
|
text: t.client_view_orders.post_button,
|
||||||
leadingIcon: UiIcons.add,
|
leadingIcon: UiIcons.add,
|
||||||
onPressed: () => Modular.to.navigateToCreateOrder(),
|
onPressed: () => Modular.to.toCreateOrder(),
|
||||||
size: UiButtonSize.small,
|
size: UiButtonSize.small,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
minimumSize: const Size(0, 48),
|
minimumSize: const Size(0, 48),
|
||||||
|
|||||||
@@ -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');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
|
|
||||||
final FirebaseAuth firebaseAuth;
|
final FirebaseAuth firebaseAuth;
|
||||||
final ExampleConnector dataConnect;
|
final ExampleConnector dataConnect;
|
||||||
|
Completer<String?>? _pendingVerification;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<domain.User?> get currentUser => firebaseAuth
|
Stream<domain.User?> get currentUser => firebaseAuth
|
||||||
@@ -39,6 +40,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
@override
|
@override
|
||||||
Future<String?> signInWithPhone({required String phoneNumber}) async {
|
Future<String?> signInWithPhone({required String phoneNumber}) async {
|
||||||
final Completer<String?> completer = Completer<String?>();
|
final Completer<String?> completer = Completer<String?>();
|
||||||
|
_pendingVerification = completer;
|
||||||
|
|
||||||
await firebaseAuth.verifyPhoneNumber(
|
await firebaseAuth.verifyPhoneNumber(
|
||||||
phoneNumber: phoneNumber,
|
phoneNumber: phoneNumber,
|
||||||
@@ -76,6 +78,15 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void cancelPendingPhoneVerification() {
|
||||||
|
final Completer<String?>? completer = _pendingVerification;
|
||||||
|
if (completer != null && !completer.isCompleted) {
|
||||||
|
completer.completeError(Exception('Phone verification cancelled.'));
|
||||||
|
}
|
||||||
|
_pendingVerification = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() {
|
Future<void> signOut() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ abstract interface class AuthRepositoryInterface {
|
|||||||
/// Signs in with a phone number and returns a verification ID.
|
/// Signs in with a phone number and returns a verification ID.
|
||||||
Future<String?> signInWithPhone({required String phoneNumber});
|
Future<String?> signInWithPhone({required String phoneNumber});
|
||||||
|
|
||||||
|
/// Cancels any pending phone verification request (if possible).
|
||||||
|
void cancelPendingPhoneVerification();
|
||||||
|
|
||||||
/// Verifies the OTP code and returns the authenticated user.
|
/// Verifies the OTP code and returns the authenticated user.
|
||||||
Future<User?> verifyOtp({
|
Future<User?> verifyOtp({
|
||||||
required String verificationId,
|
required String verificationId,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,4 +18,8 @@ class SignInWithPhoneUseCase
|
|||||||
Future<String?> call(SignInWithPhoneArguments arguments) {
|
Future<String?> call(SignInWithPhoneArguments arguments) {
|
||||||
return _repository.signInWithPhone(phoneNumber: arguments.phoneNumber);
|
return _repository.signInWithPhone(phoneNumber: arguments.phoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cancelPending() {
|
||||||
|
_repository.cancelPendingPhoneVerification();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
@@ -15,6 +16,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
|
|
||||||
/// The use case for verifying an OTP.
|
/// The use case for verifying an OTP.
|
||||||
final VerifyOtpUseCase _verifyOtpUseCase;
|
final VerifyOtpUseCase _verifyOtpUseCase;
|
||||||
|
int _requestToken = 0;
|
||||||
|
DateTime? _lastCodeRequestAt;
|
||||||
|
DateTime? _cooldownUntil;
|
||||||
|
static const Duration _resendCooldown = Duration(seconds: 31);
|
||||||
|
Timer? _cooldownTimer;
|
||||||
|
|
||||||
/// Creates an [AuthBloc].
|
/// Creates an [AuthBloc].
|
||||||
AuthBloc({
|
AuthBloc({
|
||||||
@@ -28,11 +34,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
on<AuthErrorCleared>(_onErrorCleared);
|
on<AuthErrorCleared>(_onErrorCleared);
|
||||||
on<AuthOtpUpdated>(_onOtpUpdated);
|
on<AuthOtpUpdated>(_onOtpUpdated);
|
||||||
on<AuthPhoneUpdated>(_onPhoneUpdated);
|
on<AuthPhoneUpdated>(_onPhoneUpdated);
|
||||||
|
on<AuthResetRequested>(_onResetRequested);
|
||||||
|
on<AuthCooldownTicked>(_onCooldownTicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears any authentication error from the state.
|
/// Clears any authentication error from the state.
|
||||||
void _onErrorCleared(AuthErrorCleared event, Emitter<AuthState> emit) {
|
void _onErrorCleared(AuthErrorCleared event, Emitter<AuthState> emit) {
|
||||||
emit(state.copyWith(status: AuthStatus.codeSent, errorMessage: null));
|
emit(state.copyWith(status: AuthStatus.codeSent, errorMessage: ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the internal OTP state without triggering a submission.
|
/// Updates the internal OTP state without triggering a submission.
|
||||||
@@ -41,14 +49,22 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
otp: event.otp,
|
otp: event.otp,
|
||||||
status: AuthStatus.codeSent,
|
status: AuthStatus.codeSent,
|
||||||
errorMessage: null,
|
errorMessage: '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the internal phone number state without triggering a submission.
|
/// Updates the internal phone number state without triggering a submission.
|
||||||
void _onPhoneUpdated(AuthPhoneUpdated event, Emitter<AuthState> emit) {
|
void _onPhoneUpdated(AuthPhoneUpdated event, Emitter<AuthState> emit) {
|
||||||
emit(state.copyWith(phoneNumber: event.phoneNumber, errorMessage: null));
|
emit(state.copyWith(phoneNumber: event.phoneNumber, errorMessage: ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the authentication state to initial for a given mode.
|
||||||
|
void _onResetRequested(AuthResetRequested event, Emitter<AuthState> emit) {
|
||||||
|
_requestToken++;
|
||||||
|
_signInUseCase.cancelPending();
|
||||||
|
_cancelCooldownTimer();
|
||||||
|
emit(AuthState(status: AuthStatus.initial, mode: event.mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the sign-in request, initiating the phone authentication process.
|
/// Handles the sign-in request, initiating the phone authentication process.
|
||||||
@@ -56,11 +72,37 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
AuthSignInRequested event,
|
AuthSignInRequested event,
|
||||||
Emitter<AuthState> emit,
|
Emitter<AuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
if (_lastCodeRequestAt != null) {
|
||||||
|
final DateTime cooldownUntil =
|
||||||
|
_cooldownUntil ?? _lastCodeRequestAt!.add(_resendCooldown);
|
||||||
|
final int remaining = cooldownUntil.difference(now).inSeconds;
|
||||||
|
if (remaining > 0) {
|
||||||
|
_startCooldown(remaining);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: AuthStatus.error,
|
||||||
|
mode: event.mode,
|
||||||
|
phoneNumber: event.phoneNumber ?? state.phoneNumber,
|
||||||
|
errorMessage: 'Please wait ${remaining}s before requesting a new code.',
|
||||||
|
cooldownSecondsRemaining: remaining,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_signInUseCase.cancelPending();
|
||||||
|
final int token = ++_requestToken;
|
||||||
|
_lastCodeRequestAt = now;
|
||||||
|
_cooldownUntil = now.add(_resendCooldown);
|
||||||
|
_cancelCooldownTimer();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: AuthStatus.loading,
|
status: AuthStatus.loading,
|
||||||
mode: event.mode,
|
mode: event.mode,
|
||||||
phoneNumber: event.phoneNumber,
|
phoneNumber: event.phoneNumber,
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
@@ -69,19 +111,79 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
phoneNumber: event.phoneNumber ?? state.phoneNumber,
|
phoneNumber: event.phoneNumber ?? state.phoneNumber,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (token != _requestToken) return;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: AuthStatus.codeSent,
|
status: AuthStatus.codeSent,
|
||||||
verificationId: verificationId,
|
verificationId: verificationId,
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (token != _requestToken) return;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(status: AuthStatus.error, errorMessage: e.toString()),
|
state.copyWith(
|
||||||
|
status: AuthStatus.error,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onCooldownTicked(
|
||||||
|
AuthCooldownTicked event,
|
||||||
|
Emitter<AuthState> emit,
|
||||||
|
) {
|
||||||
|
print('Auth cooldown tick: ${event.secondsRemaining}');
|
||||||
|
if (event.secondsRemaining <= 0) {
|
||||||
|
print('Auth cooldown finished: clearing message');
|
||||||
|
_cancelCooldownTimer();
|
||||||
|
_cooldownUntil = null;
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: AuthStatus.initial,
|
||||||
|
errorMessage: '',
|
||||||
|
cooldownSecondsRemaining: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: AuthStatus.error,
|
||||||
|
errorMessage:
|
||||||
|
'Please wait ${event.secondsRemaining}s before requesting a new code.',
|
||||||
|
cooldownSecondsRemaining: event.secondsRemaining,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startCooldown(int secondsRemaining) {
|
||||||
|
_cancelCooldownTimer();
|
||||||
|
int remaining = secondsRemaining;
|
||||||
|
add(AuthCooldownTicked(remaining));
|
||||||
|
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (Timer timer) {
|
||||||
|
remaining -= 1;
|
||||||
|
print('Auth cooldown timer: remaining=$remaining');
|
||||||
|
if (remaining <= 0) {
|
||||||
|
timer.cancel();
|
||||||
|
_cooldownTimer = null;
|
||||||
|
print('Auth cooldown timer: reached 0, emitting tick');
|
||||||
|
add(const AuthCooldownTicked(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
add(AuthCooldownTicked(remaining));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelCooldownTimer() {
|
||||||
|
_cooldownTimer?.cancel();
|
||||||
|
_cooldownTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Handles OTP submission and verification.
|
/// Handles OTP submission and verification.
|
||||||
Future<void> _onOtpSubmitted(
|
Future<void> _onOtpSubmitted(
|
||||||
AuthOtpSubmitted event,
|
AuthOtpSubmitted event,
|
||||||
@@ -107,6 +209,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
|
|||||||
/// Disposes the BLoC resources.
|
/// Disposes the BLoC resources.
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_cancelCooldownTimer();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,27 @@ class AuthOtpSubmitted extends AuthEvent {
|
|||||||
/// Event for clearing any authentication error in the state.
|
/// Event for clearing any authentication error in the state.
|
||||||
class AuthErrorCleared extends AuthEvent {}
|
class AuthErrorCleared extends AuthEvent {}
|
||||||
|
|
||||||
|
/// Event for resetting the authentication flow back to initial.
|
||||||
|
class AuthResetRequested extends AuthEvent {
|
||||||
|
/// The authentication mode (login or signup).
|
||||||
|
final AuthMode mode;
|
||||||
|
|
||||||
|
const AuthResetRequested({required this.mode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => <Object>[mode];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event for ticking down the resend cooldown.
|
||||||
|
class AuthCooldownTicked extends AuthEvent {
|
||||||
|
final int secondsRemaining;
|
||||||
|
|
||||||
|
const AuthCooldownTicked(this.secondsRemaining);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => <Object>[secondsRemaining];
|
||||||
|
}
|
||||||
|
|
||||||
/// Event for updating the current draft OTP in the state.
|
/// Event for updating the current draft OTP in the state.
|
||||||
class AuthOtpUpdated extends AuthEvent {
|
class AuthOtpUpdated extends AuthEvent {
|
||||||
/// The current draft OTP.
|
/// The current draft OTP.
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class AuthState extends Equatable {
|
|||||||
|
|
||||||
/// A descriptive message for any error that occurred.
|
/// A descriptive message for any error that occurred.
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// Cooldown in seconds before requesting a new code.
|
||||||
|
final int cooldownSecondsRemaining;
|
||||||
|
|
||||||
/// The authenticated user's data (available when status is [AuthStatus.authenticated]).
|
/// The authenticated user's data (available when status is [AuthStatus.authenticated]).
|
||||||
final User? user;
|
final User? user;
|
||||||
@@ -50,6 +53,7 @@ class AuthState extends Equatable {
|
|||||||
this.otp = '',
|
this.otp = '',
|
||||||
this.phoneNumber = '',
|
this.phoneNumber = '',
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.cooldownSecondsRemaining = 0,
|
||||||
this.user,
|
this.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ class AuthState extends Equatable {
|
|||||||
otp,
|
otp,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
cooldownSecondsRemaining,
|
||||||
user,
|
user,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -78,6 +83,7 @@ class AuthState extends Equatable {
|
|||||||
String? otp,
|
String? otp,
|
||||||
String? phoneNumber,
|
String? phoneNumber,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
int? cooldownSecondsRemaining,
|
||||||
User? user,
|
User? user,
|
||||||
}) {
|
}) {
|
||||||
return AuthState(
|
return AuthState(
|
||||||
@@ -87,6 +93,8 @@ class AuthState extends Equatable {
|
|||||||
otp: otp ?? this.otp,
|
otp: otp ?? this.otp,
|
||||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
cooldownSecondsRemaining:
|
||||||
|
cooldownSecondsRemaining ?? this.cooldownSecondsRemaining,
|
||||||
user: user ?? this.user,
|
user: user ?? this.user,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +26,12 @@ 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].
|
/// Error message if the profile setup fails.
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// List of location suggestions from the API.
|
||||||
|
final List<String> locationSuggestions;
|
||||||
|
|
||||||
/// Creates a [ProfileSetupState] instance.
|
/// Creates a [ProfileSetupState] instance.
|
||||||
const ProfileSetupState({
|
const ProfileSetupState({
|
||||||
this.fullName = '',
|
this.fullName = '',
|
||||||
@@ -39,6 +42,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 +55,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 +66,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,21 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
import '../../domain/ui_entities/auth_mode.dart';
|
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] to provide strongly-typed navigation
|
|
||||||
/// for the staff authentication feature.
|
|
||||||
extension AuthNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the phone verification page.
|
|
||||||
void pushPhoneVerification(AuthMode mode) {
|
|
||||||
pushNamed('./phone-verification', arguments: <String, String>{'mode': mode.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the profile setup page, replacing the current route.
|
|
||||||
void pushReplacementProfileSetup() {
|
|
||||||
pushReplacementNamed('./profile-setup');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the worker home (external to this module).
|
|
||||||
void pushWorkerHome() {
|
|
||||||
pushNamed('/worker-main/home');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:staff_authentication/src/domain/ui_entities/auth_mode.dart';
|
|
||||||
import '../navigation/auth_navigator.dart'; // Import the extension
|
import 'package:krow_core/core.dart';
|
||||||
import '../widgets/get_started_page/get_started_actions.dart';
|
import '../widgets/get_started_page/get_started_actions.dart';
|
||||||
import '../widgets/get_started_page/get_started_background.dart';
|
import '../widgets/get_started_page/get_started_background.dart';
|
||||||
import '../widgets/get_started_page/get_started_header.dart';
|
import '../widgets/get_started_page/get_started_header.dart';
|
||||||
@@ -17,12 +17,12 @@ class GetStartedPage extends StatelessWidget {
|
|||||||
|
|
||||||
/// On sign up pressed callback.
|
/// On sign up pressed callback.
|
||||||
void onSignUpPressed() {
|
void onSignUpPressed() {
|
||||||
Modular.to.pushPhoneVerification(AuthMode.signup);
|
Modular.to.toPhoneVerification('signup');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// On login pressed callback.
|
/// On login pressed callback.
|
||||||
void onLoginPressed() {
|
void onLoginPressed() {
|
||||||
Modular.to.pushPhoneVerification(AuthMode.login);
|
Modular.to.toPhoneVerification('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:staff_authentication/src/presentation/blocs/auth_event.dart';
|
|||||||
import 'package:staff_authentication/src/presentation/blocs/auth_state.dart';
|
import 'package:staff_authentication/src/presentation/blocs/auth_state.dart';
|
||||||
import 'package:staff_authentication/staff_authentication.dart';
|
import 'package:staff_authentication/staff_authentication.dart';
|
||||||
|
|
||||||
import '../navigation/auth_navigator.dart'; // Import the extension
|
import 'package:krow_core/core.dart';
|
||||||
import '../widgets/phone_verification_page/otp_verification.dart';
|
import '../widgets/phone_verification_page/otp_verification.dart';
|
||||||
import '../widgets/phone_verification_page/phone_input.dart';
|
import '../widgets/phone_verification_page/phone_input.dart';
|
||||||
|
|
||||||
@@ -15,23 +15,45 @@ import '../widgets/phone_verification_page/phone_input.dart';
|
|||||||
///
|
///
|
||||||
/// This page coordinates the authentication flow by switching between
|
/// This page coordinates the authentication flow by switching between
|
||||||
/// [PhoneInput] and [OtpVerification] based on the current [AuthState].
|
/// [PhoneInput] and [OtpVerification] based on the current [AuthState].
|
||||||
class PhoneVerificationPage extends StatelessWidget {
|
class PhoneVerificationPage extends StatefulWidget {
|
||||||
/// The authentication mode (login or signup).
|
/// The authentication mode (login or signup).
|
||||||
final AuthMode mode;
|
final AuthMode mode;
|
||||||
|
|
||||||
/// Creates a [PhoneVerificationPage].
|
/// Creates a [PhoneVerificationPage].
|
||||||
const PhoneVerificationPage({super.key, required this.mode});
|
const PhoneVerificationPage({super.key, required this.mode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PhoneVerificationPage> createState() => _PhoneVerificationPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||||
|
late final AuthBloc _authBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_authBloc = Modular.get<AuthBloc>();
|
||||||
|
_authBloc.add(AuthResetRequested(mode: widget.mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_authBloc.add(AuthResetRequested(mode: widget.mode));
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles the request to send a verification code to the provided phone number.
|
/// Handles the request to send a verification code to the provided phone number.
|
||||||
void _onSendCode({
|
void _onSendCode({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String phoneNumber,
|
required String phoneNumber,
|
||||||
}) {
|
}) {
|
||||||
print('Phone verification input: "$phoneNumber" len=${phoneNumber.length}');
|
final String normalized = phoneNumber.replaceAll(RegExp(r'\\D'), '');
|
||||||
if (phoneNumber.length == 10) {
|
if (normalized.length == 10) {
|
||||||
BlocProvider.of<AuthBloc>(
|
BlocProvider.of<AuthBloc>(
|
||||||
context,
|
context,
|
||||||
).add(AuthSignInRequested(phoneNumber: '+1$phoneNumber', mode: mode));
|
).add(
|
||||||
|
AuthSignInRequested(phoneNumber: '+1$normalized', mode: widget.mode),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -55,40 +77,49 @@ class PhoneVerificationPage extends StatelessWidget {
|
|||||||
AuthOtpSubmitted(
|
AuthOtpSubmitted(
|
||||||
verificationId: verificationId,
|
verificationId: verificationId,
|
||||||
smsCode: otp,
|
smsCode: otp,
|
||||||
mode: mode,
|
mode: widget.mode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the request to resend the verification code using the phone number in the state.
|
/// Handles the request to resend the verification code using the phone number in the state.
|
||||||
void _onResend({required BuildContext context}) {
|
void _onResend({required BuildContext context}) {
|
||||||
BlocProvider.of<AuthBloc>(context).add(AuthSignInRequested(mode: mode));
|
BlocProvider.of<AuthBloc>(context).add(
|
||||||
|
AuthSignInRequested(mode: widget.mode),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<AuthBloc>(
|
return BlocProvider<AuthBloc>.value(
|
||||||
create: (BuildContext context) => Modular.get<AuthBloc>(),
|
value: _authBloc,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return BlocListener<AuthBloc, AuthState>(
|
return BlocListener<AuthBloc, AuthState>(
|
||||||
listener: (BuildContext context, AuthState state) {
|
listener: (BuildContext context, AuthState state) {
|
||||||
if (state.status == AuthStatus.authenticated) {
|
if (state.status == AuthStatus.authenticated) {
|
||||||
if (state.mode == AuthMode.signup) {
|
if (state.mode == AuthMode.signup) {
|
||||||
Modular.to.pushReplacementProfileSetup();
|
Modular.to.toProfileSetup();
|
||||||
} else {
|
} else {
|
||||||
Modular.to.pushWorkerHome();
|
Modular.to.toStaffHome();
|
||||||
}
|
}
|
||||||
} else if (state.status == AuthStatus.error &&
|
} else if (state.status == AuthStatus.error &&
|
||||||
state.mode == AuthMode.signup) {
|
state.mode == AuthMode.signup) {
|
||||||
final String message = state.errorMessage ?? '';
|
final String message = state.errorMessage ?? '';
|
||||||
if (message.contains('staff profile')) {
|
if (message.contains('staff profile')) {
|
||||||
Modular.to.pushReplacementNamed(
|
final ScaffoldMessengerState messenger =
|
||||||
'./phone-verification',
|
ScaffoldMessenger.of(context);
|
||||||
arguments: <String, String>{
|
messenger.hideCurrentSnackBar();
|
||||||
'mode': AuthMode.login.name,
|
messenger.showSnackBar(
|
||||||
},
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
Future<void>.delayed(const Duration(seconds: 5), () {
|
||||||
|
if (!mounted) return;
|
||||||
|
Modular.to.navigate('/');
|
||||||
|
});
|
||||||
} else if (message.contains('not authorized')) {
|
} else if (message.contains('not authorized')) {
|
||||||
Modular.to.pop();
|
Modular.to.pop();
|
||||||
}
|
}
|
||||||
@@ -104,34 +135,48 @@ class PhoneVerificationPage extends StatelessWidget {
|
|||||||
(state.status == AuthStatus.loading &&
|
(state.status == AuthStatus.loading &&
|
||||||
state.verificationId != null);
|
state.verificationId != null);
|
||||||
|
|
||||||
return Scaffold(
|
return WillPopScope(
|
||||||
appBar: const UiAppBar(
|
onWillPop: () async {
|
||||||
centerTitle: true,
|
BlocProvider.of<AuthBloc>(
|
||||||
showBackButton: true,
|
context,
|
||||||
),
|
).add(AuthResetRequested(mode: widget.mode));
|
||||||
body: SafeArea(
|
return true;
|
||||||
child: isOtpStep
|
},
|
||||||
? OtpVerification(
|
child: Scaffold(
|
||||||
state: state,
|
appBar: UiAppBar(
|
||||||
onOtpSubmitted: (String otp) => _onOtpSubmitted(
|
centerTitle: true,
|
||||||
context: context,
|
showBackButton: true,
|
||||||
otp: otp,
|
onLeadingPressed: () {
|
||||||
verificationId: state.verificationId ?? '',
|
BlocProvider.of<AuthBloc>(context).add(
|
||||||
),
|
AuthResetRequested(mode: widget.mode),
|
||||||
onResend: () => _onResend(context: context),
|
);
|
||||||
onContinue: () => _onOtpSubmitted(
|
Navigator.of(context).pop();
|
||||||
context: context,
|
},
|
||||||
otp: state.otp,
|
),
|
||||||
verificationId: state.verificationId ?? '',
|
body: SafeArea(
|
||||||
),
|
child: isOtpStep
|
||||||
)
|
? OtpVerification(
|
||||||
|
state: state,
|
||||||
|
onOtpSubmitted: (String otp) => _onOtpSubmitted(
|
||||||
|
context: context,
|
||||||
|
otp: otp,
|
||||||
|
verificationId: state.verificationId ?? '',
|
||||||
|
),
|
||||||
|
onResend: () => _onResend(context: context),
|
||||||
|
onContinue: () => _onOtpSubmitted(
|
||||||
|
context: context,
|
||||||
|
otp: state.otp,
|
||||||
|
verificationId: state.verificationId ?? '',
|
||||||
|
),
|
||||||
|
)
|
||||||
: PhoneInput(
|
: PhoneInput(
|
||||||
state: state,
|
state: state,
|
||||||
onSendCode: () => _onSendCode(
|
onSendCode: (String phoneNumber) => _onSendCode(
|
||||||
context: context,
|
context: context,
|
||||||
phoneNumber: state.phoneNumber,
|
phoneNumber: phoneNumber,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import '../widgets/profile_setup_page/profile_setup_location.dart';
|
|||||||
import '../widgets/profile_setup_page/profile_setup_experience.dart';
|
import '../widgets/profile_setup_page/profile_setup_experience.dart';
|
||||||
import '../widgets/profile_setup_page/profile_setup_header.dart';
|
import '../widgets/profile_setup_page/profile_setup_header.dart';
|
||||||
import 'package:staff_authentication/staff_authentication.dart';
|
import 'package:staff_authentication/staff_authentication.dart';
|
||||||
import '../navigation/auth_navigator.dart'; // Import the extension
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
/// Page for setting up the user profile after authentication.
|
/// Page for setting up the user profile after authentication.
|
||||||
class ProfileSetupPage extends StatefulWidget {
|
class ProfileSetupPage extends StatefulWidget {
|
||||||
@@ -93,7 +93,7 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
|
|||||||
child: BlocConsumer<ProfileSetupBloc, ProfileSetupState>(
|
child: BlocConsumer<ProfileSetupBloc, ProfileSetupState>(
|
||||||
listener: (BuildContext context, ProfileSetupState state) {
|
listener: (BuildContext context, ProfileSetupState state) {
|
||||||
if (state.status == ProfileSetupStatus.success) {
|
if (state.status == ProfileSetupStatus.success) {
|
||||||
Modular.to.pushWorkerHome();
|
Modular.to.toStaffHome();
|
||||||
} else if (state.status == ProfileSetupStatus.failure) {
|
} else if (state.status == ProfileSetupStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|||||||
@@ -17,16 +17,25 @@ class PhoneInput extends StatefulWidget {
|
|||||||
final AuthState state;
|
final AuthState state;
|
||||||
|
|
||||||
/// Callback for when the "Send Code" action is triggered.
|
/// Callback for when the "Send Code" action is triggered.
|
||||||
final VoidCallback onSendCode;
|
final ValueChanged<String> onSendCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PhoneInput> createState() => _PhoneInputState();
|
State<PhoneInput> createState() => _PhoneInputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PhoneInputState extends State<PhoneInput> {
|
class _PhoneInputState extends State<PhoneInput> {
|
||||||
|
String _currentPhone = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentPhone = widget.state.phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
void _handlePhoneChanged(String value) {
|
void _handlePhoneChanged(String value) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
_currentPhone = value;
|
||||||
final AuthBloc bloc = context.read<AuthBloc>();
|
final AuthBloc bloc = context.read<AuthBloc>();
|
||||||
if (!bloc.isClosed) {
|
if (!bloc.isClosed) {
|
||||||
bloc.add(AuthPhoneUpdated(value));
|
bloc.add(AuthPhoneUpdated(value));
|
||||||
@@ -59,7 +68,7 @@ class _PhoneInputState extends State<PhoneInput> {
|
|||||||
),
|
),
|
||||||
PhoneInputActions(
|
PhoneInputActions(
|
||||||
isLoading: widget.state.isLoading,
|
isLoading: widget.state.isLoading,
|
||||||
onSendCode: widget.onSendCode,
|
onSendCode: () => widget.onSendCode(_currentPhone),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
|
|||||||
_controller = TextEditingController(text: widget.initialValue);
|
_controller = TextEditingController(text: widget.initialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PhoneInputFormField oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.initialValue != oldWidget.initialValue &&
|
||||||
|
_controller.text != widget.initialValue) {
|
||||||
|
_controller.text = widget.initialValue;
|
||||||
|
_controller.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: _controller.text.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
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/blocs/profile_setup/profile_setup_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 +35,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 +77,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: 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 +167,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 +185,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ library staff_authentication;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||||
import 'package:staff_authentication/src/data/repositories_impl/auth_repository_impl.dart';
|
import 'package:staff_authentication/src/data/repositories_impl/auth_repository_impl.dart';
|
||||||
@@ -11,6 +12,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 +48,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,15 +66,16 @@ class StaffAuthenticationModule extends Module {
|
|||||||
i.add<ProfileSetupBloc>(
|
i.add<ProfileSetupBloc>(
|
||||||
() => ProfileSetupBloc(
|
() => ProfileSetupBloc(
|
||||||
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
||||||
|
searchCities: i.get<SearchCitiesUseCase>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const GetStartedPage());
|
r.child(StaffPaths.root, child: (_) => const GetStartedPage());
|
||||||
r.child(
|
r.child(
|
||||||
'/phone-verification',
|
StaffPaths.phoneVerification,
|
||||||
child: (BuildContext context) {
|
child: (BuildContext context) {
|
||||||
final Map<String, dynamic>? data = r.args.data;
|
final Map<String, dynamic>? data = r.args.data;
|
||||||
final String? modeName = data?['mode'];
|
final String? modeName = data?['mode'];
|
||||||
@@ -79,6 +86,6 @@ class StaffAuthenticationModule extends Module {
|
|||||||
return PhoneVerificationPage(mode: mode);
|
return PhoneVerificationPage(mode: mode);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
r.child('/profile-setup', child: (_) => const ProfileSetupPage());
|
r.child(StaffPaths.profileSetup, child: (_) => const ProfileSetupPage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:staff_availability/src/presentation/pages/availability_page.dart';
|
import 'package:staff_availability/src/presentation/pages/availability_page.dart';
|
||||||
|
|
||||||
@@ -23,18 +24,21 @@ class StaffAvailabilityModule extends Module {
|
|||||||
firebaseAuth: FirebaseAuth.instance,
|
firebaseAuth: FirebaseAuth.instance,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
i.add(GetWeeklyAvailabilityUseCase.new);
|
i.add(GetWeeklyAvailabilityUseCase.new);
|
||||||
i.add(UpdateDayAvailabilityUseCase.new);
|
i.add(UpdateDayAvailabilityUseCase.new);
|
||||||
i.add(ApplyQuickSetUseCase.new);
|
i.add(ApplyQuickSetUseCase.new);
|
||||||
|
|
||||||
// BLoC
|
// BLoC
|
||||||
i.add(AvailabilityBloc.new);
|
i.add(AvailabilityBloc.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const AvailabilityPage());
|
r.child(
|
||||||
|
StaffPaths.childRoute(StaffPaths.availability, StaffPaths.availability),
|
||||||
|
child: (_) => const AvailabilityPage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
|
||||||
import 'data/repositories_impl/clock_in_repository_impl.dart';
|
import 'data/repositories_impl/clock_in_repository_impl.dart';
|
||||||
@@ -15,9 +17,7 @@ class StaffClockInModule extends Module {
|
|||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Repositories
|
// Repositories
|
||||||
i.add<ClockInRepositoryInterface>(
|
i.add<ClockInRepositoryInterface>(
|
||||||
() => ClockInRepositoryImpl(
|
() => ClockInRepositoryImpl(dataConnect: ExampleConnector.instance),
|
||||||
dataConnect: ExampleConnector.instance,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use Cases
|
// Use Cases
|
||||||
@@ -31,7 +31,10 @@ class StaffClockInModule extends Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (context) => const ClockInPage());
|
r.child(
|
||||||
|
StaffPaths.childRoute(StaffPaths.clockIn, StaffPaths.clockIn),
|
||||||
|
child: (BuildContext context) => const ClockInPage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] providing typed navigation helpers
|
|
||||||
/// for the Staff Home feature (worker home screen).
|
|
||||||
///
|
|
||||||
/// Keep routes as small wrappers around `pushNamed` / `navigate` so callers
|
|
||||||
/// don't need to rely on literal paths throughout the codebase.
|
|
||||||
extension HomeNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the worker profile page.
|
|
||||||
void pushWorkerProfile() {
|
|
||||||
pushNamed('/worker-main/profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the availability page.
|
|
||||||
void pushAvailability() {
|
|
||||||
pushNamed('/worker-main/availability');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the messages page.
|
|
||||||
void pushMessages() {
|
|
||||||
pushNamed('/messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the payments page.
|
|
||||||
void navigateToPayments() {
|
|
||||||
navigate('/worker-main/payments');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the shifts listing.
|
|
||||||
/// Optionally provide a [tab] query param (e.g. `find`).
|
|
||||||
void pushShifts({String? tab}) {
|
|
||||||
if (tab == null) {
|
|
||||||
navigate('/worker-main/shifts');
|
|
||||||
} else {
|
|
||||||
navigate('/worker-main/shifts', arguments: <String, dynamic>{
|
|
||||||
'initialTab': tab,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the settings page.
|
|
||||||
void pushSettings() {
|
|
||||||
pushNamed('/settings');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the shift details page for the given [shift].
|
|
||||||
void pushShiftDetails(Shift shift) {
|
|
||||||
pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
||||||
import 'package:staff_home/src/presentation/navigation/home_navigator.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/home_header.dart';
|
||||||
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
|
import 'package:staff_home/src/presentation/widgets/home_page/placeholder_banner.dart';
|
||||||
@@ -69,7 +69,7 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
bg: UiColors.bgHighlight,
|
bg: UiColors.bgHighlight,
|
||||||
accent: UiColors.primary,
|
accent: UiColors.primary,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Modular.to.pushWorkerProfile();
|
Modular.to.toProfile();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -85,21 +85,21 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
child: QuickActionItem(
|
child: QuickActionItem(
|
||||||
icon: LucideIcons.search,
|
icon: LucideIcons.search,
|
||||||
label: quickI18n.find_shifts,
|
label: quickI18n.find_shifts,
|
||||||
onTap: () => Modular.to.pushShifts(),
|
onTap: () => Modular.to.toShifts(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: QuickActionItem(
|
child: QuickActionItem(
|
||||||
icon: LucideIcons.calendar,
|
icon: LucideIcons.calendar,
|
||||||
label: quickI18n.availability,
|
label: quickI18n.availability,
|
||||||
onTap: () => Modular.to.pushAvailability(),
|
onTap: () => Modular.to.toAvailability(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: QuickActionItem(
|
child: QuickActionItem(
|
||||||
icon: LucideIcons.dollarSign,
|
icon: LucideIcons.dollarSign,
|
||||||
label: quickI18n.earnings,
|
label: quickI18n.earnings,
|
||||||
onTap: () => Modular.to.navigateToPayments(),
|
onTap: () => Modular.to.toPayments(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -132,7 +132,7 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
EmptyStateWidget(
|
EmptyStateWidget(
|
||||||
message: emptyI18n.no_shifts_today,
|
message: emptyI18n.no_shifts_today,
|
||||||
actionLink: emptyI18n.find_shifts_cta,
|
actionLink: emptyI18n.find_shifts_cta,
|
||||||
onAction: () => Modular.to.pushShifts(tab: 'find'),
|
onAction: () => Modular.to.toShifts(initialTab: 'find'),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:lucide_icons/lucide_icons.dart';
|
|||||||
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:staff_home/src/presentation/navigation/home_navigator.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
|
||||||
/// Card widget for displaying pending payment information, using design system tokens.
|
/// Card widget for displaying pending payment information, using design system tokens.
|
||||||
@@ -16,7 +16,7 @@ class PendingPaymentCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pendingI18n = t.staff.home.pending_payment;
|
final pendingI18n = t.staff.home.pending_payment;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Modular.to.navigateToPayments(),
|
onTap: () => Modular.to.toPayments(),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import 'package:staff_home/src/presentation/navigation/home_navigator.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
class RecommendedShiftCard extends StatelessWidget {
|
class RecommendedShiftCard extends StatelessWidget {
|
||||||
final Shift shift;
|
final Shift shift;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:intl/intl.dart';
|
|||||||
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../navigation/home_navigator.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
class ShiftCard extends StatefulWidget {
|
class ShiftCard extends StatefulWidget {
|
||||||
final Shift shift;
|
final Shift shift;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:staff_home/src/data/repositories/home_repository_impl.dart';
|
import 'package:staff_home/src/data/repositories/home_repository_impl.dart';
|
||||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||||
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
import 'package:staff_home/src/presentation/blocs/home_cubit.dart';
|
||||||
@@ -14,9 +15,7 @@ class StaffHomeModule extends Module {
|
|||||||
@override
|
@override
|
||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Repository
|
// Repository
|
||||||
i.addLazySingleton<HomeRepository>(
|
i.addLazySingleton<HomeRepository>(() => HomeRepositoryImpl());
|
||||||
() => HomeRepositoryImpl(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Presentation layer - Cubit
|
// Presentation layer - Cubit
|
||||||
i.addSingleton(HomeCubit.new);
|
i.addSingleton(HomeCubit.new);
|
||||||
@@ -24,6 +23,9 @@ class StaffHomeModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (BuildContext context) => const WorkerHomePage());
|
r.child(
|
||||||
|
StaffPaths.childRoute(StaffPaths.home, StaffPaths.home),
|
||||||
|
child: (BuildContext context) => const WorkerHomePage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'domain/repositories/payments_repository.dart';
|
import 'domain/repositories/payments_repository.dart';
|
||||||
import 'domain/usecases/get_payment_summary_usecase.dart';
|
import 'domain/usecases/get_payment_summary_usecase.dart';
|
||||||
@@ -10,19 +11,22 @@ import 'presentation/pages/payments_page.dart';
|
|||||||
class StaffPaymentsModule extends Module {
|
class StaffPaymentsModule extends Module {
|
||||||
@override
|
@override
|
||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Repositories
|
// Repositories
|
||||||
i.add<PaymentsRepository>(PaymentsRepositoryImpl.new);
|
i.add<PaymentsRepository>(PaymentsRepositoryImpl.new);
|
||||||
|
|
||||||
// Use Cases
|
// Use Cases
|
||||||
i.add(GetPaymentSummaryUseCase.new);
|
i.add(GetPaymentSummaryUseCase.new);
|
||||||
i.add(GetPaymentHistoryUseCase.new);
|
i.add(GetPaymentHistoryUseCase.new);
|
||||||
|
|
||||||
// Blocs
|
// Blocs
|
||||||
i.add(PaymentsBloc.new);
|
i.add(PaymentsBloc.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (context) => const PaymentsPage());
|
r.child(
|
||||||
|
StaffPaths.childRoute(StaffPaths.payments, StaffPaths.payments),
|
||||||
|
child: (context) => const PaymentsPage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] providing typed navigation helpers
|
|
||||||
/// for the Staff Profile feature.
|
|
||||||
///
|
|
||||||
/// These methods provide a type-safe way to navigate to various profile-related
|
|
||||||
/// pages without relying on string literals throughout the codebase.
|
|
||||||
extension ProfileNavigator on IModularNavigator {
|
|
||||||
/// Navigates to the personal info page.
|
|
||||||
void pushPersonalInfo() {
|
|
||||||
pushNamed('../onboarding/personal-info');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the emergency contact page.
|
|
||||||
void pushEmergencyContact() {
|
|
||||||
pushNamed('../emergency-contact');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the experience page.
|
|
||||||
void pushExperience() {
|
|
||||||
pushNamed('../experience');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the attire page.
|
|
||||||
void pushAttire() {
|
|
||||||
pushNamed('../attire');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the documents page.
|
|
||||||
void pushDocuments() {
|
|
||||||
pushNamed('../documents');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the certificates page.
|
|
||||||
void pushCertificates() {
|
|
||||||
pushNamed('/certificates');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the tax forms page.
|
|
||||||
void pushTaxForms() {
|
|
||||||
pushNamed('../tax-forms/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to Krow University.
|
|
||||||
void pushKrowUniversity() {
|
|
||||||
pushNamed('/krow-university');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the trainings page.
|
|
||||||
void pushTrainings() {
|
|
||||||
pushNamed('/trainings');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the leaderboard page.
|
|
||||||
void pushLeaderboard() {
|
|
||||||
pushNamed('/leaderboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the bank account page.
|
|
||||||
void pushBankAccount() {
|
|
||||||
pushNamed('../bank-account/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the timecard page.
|
|
||||||
void pushTimecard() {
|
|
||||||
pushNamed('../time-card');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the FAQs page.
|
|
||||||
void pushFaqs() {
|
|
||||||
pushNamed('/faqs');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the privacy & security page.
|
|
||||||
void pushPrivacy() {
|
|
||||||
pushNamed('/privacy');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the messages page.
|
|
||||||
void pushMessages() {
|
|
||||||
pushNamed('/messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigates to the get started/authentication screen.
|
|
||||||
void navigateToGetStarted() {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
|
|
||||||
import '../blocs/profile_cubit.dart';
|
import '../blocs/profile_cubit.dart';
|
||||||
import '../blocs/profile_state.dart';
|
import '../blocs/profile_state.dart';
|
||||||
import '../navigation/profile_navigator.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import '../widgets/logout_button.dart';
|
import '../widgets/logout_button.dart';
|
||||||
import '../widgets/profile_menu_grid.dart';
|
import '../widgets/profile_menu_grid.dart';
|
||||||
import '../widgets/profile_menu_item.dart';
|
import '../widgets/profile_menu_item.dart';
|
||||||
@@ -61,7 +61,7 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
bloc: cubit,
|
bloc: cubit,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == ProfileStatus.signedOut) {
|
if (state.status == ProfileStatus.signedOut) {
|
||||||
Modular.to.navigateToGetStarted();
|
Modular.to.toGetStarted();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@@ -124,17 +124,17 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.user,
|
icon: UiIcons.user,
|
||||||
label: i18n.menu_items.personal_info,
|
label: i18n.menu_items.personal_info,
|
||||||
onTap: () => Modular.to.pushPersonalInfo(),
|
onTap: () => Modular.to.toPersonalInfo(),
|
||||||
),
|
),
|
||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.phone,
|
icon: UiIcons.phone,
|
||||||
label: i18n.menu_items.emergency_contact,
|
label: i18n.menu_items.emergency_contact,
|
||||||
onTap: () => Modular.to.pushEmergencyContact(),
|
onTap: () => Modular.to.toEmergencyContact(),
|
||||||
),
|
),
|
||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.briefcase,
|
icon: UiIcons.briefcase,
|
||||||
label: i18n.menu_items.experience,
|
label: i18n.menu_items.experience,
|
||||||
onTap: () => Modular.to.pushExperience(),
|
onTap: () => Modular.to.toExperience(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -149,7 +149,7 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.file,
|
icon: UiIcons.file,
|
||||||
label: i18n.menu_items.tax_forms,
|
label: i18n.menu_items.tax_forms,
|
||||||
onTap: () => Modular.to.pushTaxForms(),
|
onTap: () => Modular.to.toTaxForms(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -163,17 +163,17 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.building,
|
icon: UiIcons.building,
|
||||||
label: i18n.menu_items.bank_account,
|
label: i18n.menu_items.bank_account,
|
||||||
onTap: () => Modular.to.pushBankAccount(),
|
onTap: () => Modular.to.toBankAccount(),
|
||||||
),
|
),
|
||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.creditCard,
|
icon: UiIcons.creditCard,
|
||||||
label: i18n.menu_items.payments,
|
label: i18n.menu_items.payments,
|
||||||
onTap: () => Modular.to.navigate('/worker-main/payments'),
|
onTap: () => Modular.to.toPayments(),
|
||||||
),
|
),
|
||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.clock,
|
icon: UiIcons.clock,
|
||||||
label: i18n.menu_items.timecard,
|
label: i18n.menu_items.timecard,
|
||||||
onTap: () => Modular.to.pushTimecard(),
|
onTap: () => Modular.to.toTimeCard(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
@@ -40,15 +41,15 @@ class StaffProfileModule extends Module {
|
|||||||
|
|
||||||
// Presentation layer - Cubit depends on use cases
|
// Presentation layer - Cubit depends on use cases
|
||||||
i.add<ProfileCubit>(
|
i.add<ProfileCubit>(
|
||||||
() => ProfileCubit(
|
() => ProfileCubit(i.get<GetProfileUseCase>(), i.get<SignOutUseCase>()),
|
||||||
i.get<GetProfileUseCase>(),
|
|
||||||
i.get<SignOutUseCase>(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (BuildContext context) => const StaffProfilePage());
|
r.child(
|
||||||
|
StaffPaths.childRoute(StaffPaths.profile, StaffPaths.profile),
|
||||||
|
child: (BuildContext context) => const StaffProfilePage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] to provide strongly-typed navigation
|
|
||||||
/// for the staff certificates feature.
|
|
||||||
extension CertificatesNavigator on IModularNavigator {
|
|
||||||
/// Navigates back.
|
|
||||||
void popCertificates() {
|
|
||||||
pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
|
||||||
import 'data/repositories_impl/certificates_repository_impl.dart';
|
import 'data/repositories_impl/certificates_repository_impl.dart';
|
||||||
@@ -23,6 +24,9 @@ class StaffCertificatesModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const CertificatesPage());
|
r.child(
|
||||||
|
StaffPaths.childRoute(StaffPaths.certificates, StaffPaths.certificates),
|
||||||
|
child: (_) => const CertificatesPage(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
library staff_certificates;
|
library staff_certificates;
|
||||||
|
|
||||||
export 'src/staff_certificates_module.dart';
|
export 'src/staff_certificates_module.dart';
|
||||||
export 'src/presentation/navigation/certificates_navigator.dart'; // Export navigator extension
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user