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:
Achintha Isuru
2026-02-05 22:24:05 -05:00
committed by GitHub
124 changed files with 2639 additions and 991 deletions

View File

@@ -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

View File

@@ -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(),
); );
} }

View File

@@ -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());
} }
} }

View File

@@ -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';

View 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');
}

View File

@@ -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;
}
}
}

View File

@@ -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(),
);
}
}
}

View 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);
}
}

View File

@@ -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';
}

View File

@@ -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;
}
}

View 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';

View 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);
}
}

View 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';
}

View File

@@ -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

View File

@@ -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';

View File

@@ -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;
}
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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());
} }
} }

View File

@@ -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(
emit: emit,
action: () async {
final user = await _signInWithEmail(
SignInWithEmailArguments(email: event.email, password: event.password), SignInWithEmailArguments(email: event.email, password: event.password),
); );
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientAuthStatus.error, status: ClientAuthStatus.error,
errorMessage: e.messageKey, 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,8 +75,11 @@ 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(
emit: emit,
action: () async {
final user = await _signUpWithEmail(
SignUpWithEmailArguments( SignUpWithEmailArguments(
companyName: event.companyName, companyName: event.companyName,
email: event.email, email: event.email,
@@ -92,23 +87,12 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
), ),
); );
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientAuthStatus.error, status: ClientAuthStatus.error,
errorMessage: e.messageKey, 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(
emit: emit,
action: () async {
final user = await _signInWithSocial(
SignInWithSocialArguments(provider: event.provider), SignInWithSocialArguments(provider: event.provider),
); );
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientAuthStatus.error, status: ClientAuthStatus.error,
errorMessage: e.messageKey, 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 handleError(
emit: emit,
action: () async {
await _signOut(); await _signOut();
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null)); emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientAuthStatus.error, status: ClientAuthStatus.error,
errorMessage: e.messageKey, errorMessage: errorKey,
), ),
); );
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: 'errors.generic.unknown',
),
);
}
} }
} }

View File

@@ -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');
}
}

View File

@@ -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,
), ),
], ],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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:

View File

@@ -1,4 +1,3 @@
library; library;
export 'src/presentation/navigation/billing_navigator.dart';
export 'src/billing_module.dart'; export 'src/billing_module.dart';

View File

@@ -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());
} }
} }

View File

@@ -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/');
}

View File

@@ -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(

View File

@@ -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());
} }
} }

View File

@@ -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(

View File

@@ -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,

View File

@@ -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';

View File

@@ -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'),
), ),

View File

@@ -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

View File

@@ -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/');
}
}

View File

@@ -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(),
); );
} }

View File

@@ -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');
}
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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;
} }
}, },

View File

@@ -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(

View File

@@ -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

View File

@@ -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(),
);
} }
} }

View File

@@ -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(),
), ),
], ],
), ),

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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());
} }
} }

View File

@@ -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 {

View File

@@ -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: emit,
action: () async {
final hubs = await _getHubsUseCase();
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs)); emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientHubsStatus.failure, status: ClientHubsStatus.failure,
errorMessage: e.messageKey, 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,7 +85,10 @@ 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 handleError(
emit: emit,
action: () async {
await _createHubUseCase( await _createHubUseCase(
CreateHubArguments( CreateHubArguments(
name: event.name, name: event.name,
@@ -108,7 +103,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
zipCode: event.zipCode, zipCode: event.zipCode,
), ),
); );
final List<Hub> hubs = await _getHubsUseCase(); final hubs = await _getHubsUseCase();
emit( emit(
state.copyWith( state.copyWith(
status: ClientHubsStatus.actionSuccess, status: ClientHubsStatus.actionSuccess,
@@ -117,23 +112,12 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
showAddHubDialog: false, showAddHubDialog: false,
), ),
); );
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure, status: ClientHubsStatus.actionFailure,
errorMessage: e.messageKey, 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,9 +125,12 @@ 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 handleError(
emit: emit,
action: () async {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId)); await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final List<Hub> hubs = await _getHubsUseCase(); final hubs = await _getHubsUseCase();
emit( emit(
state.copyWith( state.copyWith(
status: ClientHubsStatus.actionSuccess, status: ClientHubsStatus.actionSuccess,
@@ -151,23 +138,12 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
successMessage: 'Hub deleted successfully', successMessage: 'Hub deleted successfully',
), ),
); );
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure, status: ClientHubsStatus.actionFailure,
errorMessage: e.messageKey, 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,11 +151,14 @@ 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 handleError(
emit: emit,
action: () async {
await _assignNfcTagUseCase( await _assignNfcTagUseCase(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId), AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
); );
final List<Hub> hubs = await _getHubsUseCase(); final hubs = await _getHubsUseCase();
emit( emit(
state.copyWith( state.copyWith(
status: ClientHubsStatus.actionSuccess, status: ClientHubsStatus.actionSuccess,
@@ -188,23 +167,12 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
clearHubToIdentify: true, clearHubToIdentify: true,
), ),
); );
} on AppException catch (e) { },
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); onError: (errorKey) => state.copyWith(
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure, status: ClientHubsStatus.actionFailure,
errorMessage: e.messageKey, 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(

View File

@@ -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/');
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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'];
} }

View File

@@ -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(),
);
} }
} }

View File

@@ -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/');
}
}

View File

@@ -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(

View File

@@ -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),

View File

@@ -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(

View File

@@ -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(

View File

@@ -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),
) )

View File

@@ -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');
}
}

View File

@@ -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(),
), ),
], ],
), ),

View File

@@ -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),

View File

@@ -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');
},
);
});
}

View File

@@ -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() {

View File

@@ -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;
}
}
}

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
} }

View File

@@ -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();
} }
} }

View File

@@ -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.

View File

@@ -40,6 +40,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,
); );
} }

View File

@@ -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: []));
}
} }

View File

@@ -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.

View File

@@ -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,6 +66,7 @@ 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,
); );
} }
@@ -74,5 +80,6 @@ class ProfileSetupState extends Equatable {
industries, industries,
status, status,
errorMessage, errorMessage,
locationSuggestions,
]; ];
} }

View File

@@ -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');
}
}

View File

@@ -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

View File

@@ -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,10 +135,23 @@ 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 {
BlocProvider.of<AuthBloc>(
context,
).add(AuthResetRequested(mode: widget.mode));
return true;
},
child: Scaffold(
appBar: UiAppBar(
centerTitle: true, centerTitle: true,
showBackButton: true, showBackButton: true,
onLeadingPressed: () {
BlocProvider.of<AuthBloc>(context).add(
AuthResetRequested(mode: widget.mode),
);
Navigator.of(context).pop();
},
), ),
body: SafeArea( body: SafeArea(
child: isOtpStep child: isOtpStep
@@ -127,9 +171,10 @@ class PhoneVerificationPage extends StatelessWidget {
) )
: PhoneInput( : PhoneInput(
state: state, state: state,
onSendCode: () => _onSendCode( onSendCode: (String phoneNumber) => _onSendCode(
context: context, context: context,
phoneNumber: state.phoneNumber, phoneNumber: phoneNumber,
),
), ),
), ),
), ),

View File

@@ -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(

View File

@@ -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),
), ),
], ],
); );

View File

@@ -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();

View File

@@ -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,38 +77,56 @@ 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,
children: <Widget>[
Expanded(
child: UiTextField(
label: t
.staff_authentication
.profile_setup_page
.location
.add_location_label, .add_location_label,
controller: _locationController, controller: _locationController,
hintText: t hintText: t.staff_authentication.profile_setup_page.location
.staff_authentication
.profile_setup_page
.location
.add_location_hint, .add_location_hint,
onSubmitted: (_) => _addLocation(), onChanged: _onSearchChanged,
),
),
UiButton.secondary(
text:
t.staff_authentication.profile_setup_page.location.add_button,
onPressed: _addLocation,
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 48),
maximumSize: const Size(double.infinity, 48),
), ),
// Suggestions List
BlocBuilder<ProfileSetupBloc, ProfileSetupState>(
buildWhen: (previous, current) =>
previous.locationSuggestions != current.locationSuggestions,
builder: (context, state) {
if (state.locationSuggestions.isEmpty) {
return const SizedBox.shrink();
}
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(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: state.locationSuggestions.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final suggestion = state.locationSuggestions[index];
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);
} }
} }

View File

@@ -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());
} }
} }

View File

@@ -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:

View File

@@ -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';
@@ -35,6 +36,9 @@ class StaffAvailabilityModule extends Module {
@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(),
);
} }
} }

View File

@@ -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(),
);
} }
} }

View File

@@ -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);
}
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(),
);
} }
} }

View File

@@ -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';
@@ -23,6 +24,9 @@ class StaffPaymentsModule extends Module {
@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(),
);
} }
} }

View File

@@ -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('/');
}
}

View File

@@ -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(),
), ),
], ],
), ),

View File

@@ -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(),
);
} }
} }

View File

@@ -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();
}
}

View File

@@ -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(),
);
} }
} }

View File

@@ -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