diff --git a/apps/mobile/apps/client/lib/main.dart b/apps/mobile/apps/client/lib/main.dart index 1ff20926..ba82fce4 100644 --- a/apps/mobile/apps/client/lib/main.dart +++ b/apps/mobile/apps/client/lib/main.dart @@ -6,7 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:client_authentication/client_authentication.dart' as client_authentication; -import 'package:client_home/client_home.dart' as client_home; +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' @@ -29,8 +29,8 @@ class AppModule extends Module { // Initial route points to the client authentication flow r.module('/', module: client_authentication.ClientAuthenticationModule()); - // Client home route - r.module('/client-home', module: client_home.ClientHomeModule()); + // Client main shell with bottom navigation (includes home as a child) + r.module('/client-main', module: client_main.ClientMainModule()); // Client settings route r.module( diff --git a/apps/mobile/apps/client/pubspec.yaml b/apps/mobile/apps/client/pubspec.yaml index 2a2760c8..e62cb00c 100644 --- a/apps/mobile/apps/client/pubspec.yaml +++ b/apps/mobile/apps/client/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: # Feature Packages client_authentication: path: ../../packages/features/client/authentication + client_main: + path: ../../packages/features/client/client_main client_home: path: ../../packages/features/client/home client_settings: diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 81167fa3..0d5db935 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -280,13 +280,17 @@ "end_label": "End", "workers_label": "Workers", "lunch_break_label": "Lunch Break", + "no_break": "No break", + "paid_break": "min (Paid)", + "unpaid_break": "min (Unpaid)", "different_location": "Use different location for this position", "different_location_title": "Different Location", "different_location_hint": "Enter different address", "create_order": "Create Order", "creating": "Creating...", "success_title": "Order Created!", - "success_message": "Your shift request has been posted. Workers will start applying soon." + "success_message": "Your shift request has been posted. Workers will start applying soon.", + "back_to_orders": "Back to Orders" }, "recurring": { "title": "Recurring Order", @@ -298,6 +302,15 @@ "subtitle": "Long-term staffing placement", "placeholder": "Permanent Order Flow (Work in Progress)" } + }, + "client_main": { + "tabs": { + "coverage": "Coverage", + "billing": "Billing", + "home": "Home", + "orders": "Orders", + "reports": "Reports" + } } } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index d207dc0b..fcabb08d 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -286,7 +286,11 @@ "create_order": "Crear Orden", "creating": "Creando...", "success_title": "¡Orden Creada!", - "success_message": "Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto." + "success_message": "Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto.", + "back_to_orders": "Volver a Órdenes", + "no_break": "Sin descanso", + "paid_break": "min (Pagado)", + "unpaid_break": "min (No pagado)" }, "recurring": { "title": "Orden Recurrente", @@ -298,5 +302,14 @@ "subtitle": "Colocación de personal a largo plazo", "placeholder": "Flujo de Orden Permanente (Trabajo en Progreso)" } + }, + "client_main": { + "tabs": { + "coverage": "Cobertura", + "billing": "Facturación", + "home": "Inicio", + "orders": "Órdenes", + "reports": "Reportes" + } } } diff --git a/apps/mobile/packages/design_system/lib/src/ui_icons.dart b/apps/mobile/packages/design_system/lib/src/ui_icons.dart index 60b6fb02..c24c5140 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_icons.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_icons.dart @@ -78,6 +78,9 @@ class UiIcons { /// Chevron left icon static const IconData chevronLeft = _IconLib.chevronLeft; + /// Chevron down icon + static const IconData chevronDown = _IconLib.chevronDown; + // --- Status & Feedback --- /// Info icon @@ -177,4 +180,7 @@ class UiIcons { /// NFC icon static const IconData nfc = _IconLib.nfc; + + /// Chart icon for reports + static const IconData chart = _IconLib.barChart3; } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/navigation/client_auth_navigator.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/navigation/client_auth_navigator.dart index a1cb7365..472d4707 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/navigation/client_auth_navigator.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/navigation/client_auth_navigator.dart @@ -18,8 +18,9 @@ extension ClientAuthNavigator on IModularNavigator { /// Navigates to the main client home dashboard. /// - /// Uses absolute path navigation to reset the navigation stack if necessary. + /// Uses absolute path navigation to the client main shell, + /// which will display the home tab by default. void navigateClientHome() { - navigate('/client-home'); + navigate('/client-main/home'); } } diff --git a/apps/mobile/packages/features/client/client_main/lib/client_main.dart b/apps/mobile/packages/features/client/client_main/lib/client_main.dart new file mode 100644 index 00000000..3cf2c937 --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/client_main.dart @@ -0,0 +1,4 @@ +library; + +export 'src/client_main_module.dart'; +export 'src/presentation/navigation/client_main_navigator.dart'; diff --git a/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart new file mode 100644 index 00000000..60337e31 --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart @@ -0,0 +1,46 @@ +import 'package:client_home/client_home.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +import 'presentation/blocs/client_main_cubit.dart'; +import 'presentation/pages/client_main_page.dart'; +import 'presentation/pages/placeholder_page.dart'; + +class ClientMainModule extends Module { + @override + void binds(Injector i) { + i.addSingleton(ClientMainCubit.new); + } + + @override + void routes(RouteManager r) { + r.child( + '/', + child: (BuildContext context) => const ClientMainPage(), + children: >[ + ModuleRoute('/home', module: ClientHomeModule()), + // Placeholders for other tabs + ChildRoute( + '/coverage', + child: (BuildContext context) => + const PlaceholderPage(title: 'Coverage'), + ), + ChildRoute( + '/billing', + child: (BuildContext context) => + const PlaceholderPage(title: 'Billing'), + ), + ChildRoute( + '/orders', + child: (BuildContext context) => + const PlaceholderPage(title: 'Orders'), + ), + ChildRoute( + '/reports', + child: (BuildContext context) => + const PlaceholderPage(title: 'Reports'), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/blocs/client_main_cubit.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/blocs/client_main_cubit.dart new file mode 100644 index 00000000..1d68e240 --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/blocs/client_main_cubit.dart @@ -0,0 +1,62 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'client_main_state.dart'; + +class ClientMainCubit extends Cubit implements Disposable { + ClientMainCubit() : super(const ClientMainState()) { + Modular.to.addListener(_onRouteChanged); + _onRouteChanged(); + } + + void _onRouteChanged() { + final String path = Modular.to.path; + int newIndex = state.currentIndex; + + // Detect which tab is active based on the route path + // Using contains() to handle child routes and trailing slashes + if (path.contains('/client-main/coverage')) { + newIndex = 0; + } else if (path.contains('/client-main/billing')) { + newIndex = 1; + } else if (path.contains('/client-main/home')) { + newIndex = 2; + } else if (path.contains('/client-main/orders')) { + newIndex = 3; + } else if (path.contains('/client-main/reports')) { + newIndex = 4; + } + + if (newIndex != state.currentIndex) { + emit(state.copyWith(currentIndex: newIndex)); + } + } + + void navigateToTab(int index) { + if (index == state.currentIndex) return; + + switch (index) { + case 0: + Modular.to.navigate('/client-main/coverage'); + break; + case 1: + Modular.to.navigate('/client-main/billing'); + break; + case 2: + Modular.to.navigate('/client-main/home'); + break; + case 3: + Modular.to.navigate('/client-main/orders'); + break; + case 4: + Modular.to.navigate('/client-main/reports'); + break; + } + // State update will happen via _onRouteChanged + } + + @override + void dispose() { + Modular.to.removeListener(_onRouteChanged); + close(); + } +} diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/blocs/client_main_state.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/blocs/client_main_state.dart new file mode 100644 index 00000000..f2573616 --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/blocs/client_main_state.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +class ClientMainState extends Equatable { + const ClientMainState({ + this.currentIndex = 2, // Default to Home + }); + + final int currentIndex; + + ClientMainState copyWith({int? currentIndex}) { + return ClientMainState(currentIndex: currentIndex ?? this.currentIndex); + } + + @override + List get props => [currentIndex]; +} diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/navigation/client_main_navigator.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/navigation/client_main_navigator.dart new file mode 100644 index 00000000..a0102f90 --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/navigation/client_main_navigator.dart @@ -0,0 +1,10 @@ +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/'); + } +} diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/client_main_page.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/client_main_page.dart new file mode 100644 index 00000000..1429a78f --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/client_main_page.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; + +import '../blocs/client_main_cubit.dart'; +import '../blocs/client_main_state.dart'; +import '../widgets/client_main_bottom_bar.dart'; + +/// The main page for the Client app, acting as a shell for the bottom navigation. +/// +/// It follows KROW Clean Architecture by: +/// - Being a [StatelessWidget]. +/// - Delegating state management to [ClientMainCubit]. +/// - Using [RouterOutlet] for nested navigation. +class ClientMainPage extends StatelessWidget { + const ClientMainPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => Modular.get(), + child: Scaffold( + extendBody: true, + body: const RouterOutlet(), + bottomNavigationBar: BlocBuilder( + builder: (BuildContext context, ClientMainState state) { + return ClientMainBottomBar( + currentIndex: state.currentIndex, + onTap: (int index) { + BlocProvider.of(context).navigateToTab(index); + }, + ); + }, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/placeholder_page.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/placeholder_page.dart new file mode 100644 index 00000000..18b9795d --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/pages/placeholder_page.dart @@ -0,0 +1,33 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A placeholder page for features that are not yet implemented. +/// +/// This page displays a simple message indicating that the feature +/// is coming soon. It follows the KROW Design System guidelines by: +/// - Using [UiAppBar] for the app bar +/// - Using [UiTypography] for text styling +/// - Using [UiColors] via typography extensions +class PlaceholderPage extends StatelessWidget { + /// Creates a [PlaceholderPage]. + /// + /// The [title] is displayed in the app bar and used in the + /// "coming soon" message. + const PlaceholderPage({required this.title, super.key}); + + /// The title of the feature being displayed. + final String title; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: UiAppBar(title: title), + body: Center( + child: Text( + '$title Feature Coming Soon', + style: UiTypography.body1r.textPrimary, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_main/lib/src/presentation/widgets/client_main_bottom_bar.dart b/apps/mobile/packages/features/client/client_main/lib/src/presentation/widgets/client_main_bottom_bar.dart new file mode 100644 index 00000000..e59987cf --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/lib/src/presentation/widgets/client_main_bottom_bar.dart @@ -0,0 +1,156 @@ +import 'dart:ui'; + +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A custom bottom navigation bar for the Client app. +/// +/// This widget provides a glassmorphic bottom navigation bar with blur effect +/// and follows the KROW Design System guidelines. It displays five tabs: +/// Coverage, Billing, Home, Orders, and Reports. +/// +/// The widget uses: +/// - [UiColors] for all color values +/// - [UiTypography] for text styling +/// - [UiIcons] for icon assets +/// - [UiConstants] for spacing and sizing +class ClientMainBottomBar extends StatelessWidget { + /// Creates a [ClientMainBottomBar]. + /// + /// The [currentIndex] indicates which tab is currently selected. + /// The [onTap] callback is invoked when a tab is tapped. + const ClientMainBottomBar({ + required this.currentIndex, + required this.onTap, + super.key, + }); + + /// The index of the currently selected tab. + final int currentIndex; + + /// Callback invoked when a tab is tapped. + /// + /// The callback receives the index of the tapped tab. + final ValueChanged onTap; + + @override + Widget build(BuildContext context) { + // Client App colors from design system + const Color activeColor = UiColors.textPrimary; + const Color inactiveColor = UiColors.textInactive; + + return Stack( + clipBehavior: Clip.none, + children: [ + // Glassmorphic background with blur effect + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.85), + border: Border( + top: BorderSide( + color: UiColors.black.withValues(alpha: 0.1), + ), + ), + ), + ), + ), + ), + ), + // Navigation items + Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + UiConstants.space2, + top: UiConstants.space4, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildNavItem( + index: 0, + icon: UiIcons.calendar, + label: t.client_main.tabs.coverage, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 1, + icon: UiIcons.dollar, + label: t.client_main.tabs.billing, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 2, + icon: UiIcons.building, + label: t.client_main.tabs.home, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 3, + icon: UiIcons.file, + label: t.client_main.tabs.orders, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + _buildNavItem( + index: 4, + icon: UiIcons.chart, + label: t.client_main.tabs.reports, + activeColor: activeColor, + inactiveColor: inactiveColor, + ), + ], + ), + ), + ], + ); + } + + /// Builds a single navigation item. + /// + /// Uses design system tokens for all styling: + /// - Icon size uses a standard value (24px is acceptable for navigation icons) + /// - Spacing uses [UiConstants.space1] + /// - Typography uses [UiTypography.footnote2m] + /// - Colors are passed as parameters from design system + Widget _buildNavItem({ + required int index, + required IconData icon, + required String label, + required Color activeColor, + required Color inactiveColor, + }) { + final bool isSelected = currentIndex == index; + return Expanded( + child: GestureDetector( + onTap: () => onTap(index), + behavior: HitTestBehavior.opaque, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + icon, + color: isSelected ? activeColor : inactiveColor, + size: 24, // Standard navigation icon size + ), + const SizedBox(height: UiConstants.space1), + Text( + label, + style: UiTypography.footnote2m.copyWith( + color: isSelected ? activeColor : inactiveColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/client_main/pubspec.yaml b/apps/mobile/packages/features/client/client_main/pubspec.yaml new file mode 100644 index 00000000..48a037b6 --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/pubspec.yaml @@ -0,0 +1,38 @@ +name: client_main +description: Main shell and navigation for the client application. +version: 0.0.1 +publish_to: none +resolution: workspace + +environment: + sdk: '>=3.10.0 <4.0.0' + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_bloc: ^8.1.0 + flutter_modular: ^6.3.0 + equatable: ^2.0.5 + lucide_icons: ^0.257.0 + + # Architecture Packages + design_system: + path: ../../../design_system + core_localization: + path: ../../../core_localization + client_home: + path: ../home + # Intentionally commenting these out as they might not exist yet + # client_settings: + # path: ../settings + +dev_dependencies: + flutter_test: + sdk: flutter + bloc_test: ^9.1.0 + mocktail: ^1.0.0 + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true diff --git a/apps/mobile/packages/features/client/client_main/test/presentation/blocs/client_main_cubit_test.dart b/apps/mobile/packages/features/client/client_main/test/presentation/blocs/client_main_cubit_test.dart new file mode 100644 index 00000000..6b6ecee7 --- /dev/null +++ b/apps/mobile/packages/features/client/client_main/test/presentation/blocs/client_main_cubit_test.dart @@ -0,0 +1,38 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:client_main/src/presentation/blocs/client_main_cubit.dart'; +import 'package:client_main/src/presentation/blocs/client_main_state.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockIModularNavigator extends Mock implements IModularNavigator {} + +void main() { + group('ClientMainCubit', () { + late MockIModularNavigator navigator; + + setUp(() { + navigator = MockIModularNavigator(); + when(() => navigator.path).thenReturn('/home'); + when(() => navigator.addListener(any())).thenReturn(null); + // Stub addListener to avoid errors when Cubit adds listener + // Note: addListener might be on Modular directly or via some other mechanic, + // but for this unit test we just want to suppress errors if possible or let the Cubit work. + // Actually Modular.to.addListener calls Modular.navigatorDelegate.addListener if it exists? + // Modular.to.addListener uses the internal RouterDelegate. + // Mocking Modular internals is hard. + + // Let's rely on the fact that we mocked navigatorDelegate. + Modular.navigatorDelegate = navigator; + }); + + test('initial state is correct', () { + final cubit = ClientMainCubit(); + expect(cubit.state, const ClientMainState(currentIndex: 2)); + cubit.close(); + }); + + // Note: Testing actual route changes requires more complex Modular mocking + // or integration tests, but the structure allows it. + }); +} diff --git a/apps/mobile/packages/features/client/create_order/lib/client_create_order.dart b/apps/mobile/packages/features/client/create_order/lib/client_create_order.dart index 5ceb799b..777d3b29 100644 --- a/apps/mobile/packages/features/client/create_order/lib/client_create_order.dart +++ b/apps/mobile/packages/features/client/create_order/lib/client_create_order.dart @@ -1,3 +1,4 @@ -library client_create_order; +/// Library for the Client Create Order feature. +library; export 'src/create_order_module.dart'; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart b/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart index dc353045..348cd860 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart @@ -29,7 +29,8 @@ class ClientCreateOrderModule extends Module { // Repositories i.addLazySingleton( () => ClientCreateOrderRepositoryImpl( - orderMock: i.get()), + orderMock: i.get(), + ), ); // UseCases @@ -45,14 +46,22 @@ class ClientCreateOrderModule extends Module { @override void routes(RouteManager r) { - r.child('/', - child: (BuildContext context) => const ClientCreateOrderPage()); + r.child( + '/', + child: (BuildContext context) => const ClientCreateOrderPage(), + ); r.child('/rapid', child: (BuildContext context) => const RapidOrderPage()); - r.child('/one-time', - child: (BuildContext context) => const OneTimeOrderPage()); - r.child('/recurring', - child: (BuildContext context) => const RecurringOrderPage()); - r.child('/permanent', - child: (BuildContext context) => const PermanentOrderPage()); + r.child( + '/one-time', + child: (BuildContext context) => const OneTimeOrderPage(), + ); + r.child( + '/recurring', + child: (BuildContext context) => const RecurringOrderPage(), + ); + r.child( + '/permanent', + child: (BuildContext context) => const PermanentOrderPage(), + ); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart index e0f7d843..ce1b7095 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -4,30 +4,37 @@ import '../../domain/repositories/client_create_order_repository_interface.dart' /// Implementation of [ClientCreateOrderRepositoryInterface]. /// -/// This implementation delegates all data access to the Data Connect layer, -/// specifically using [OrderRepositoryMock] for now as per the platform's mocking strategy. +/// This implementation coordinates data access for order creation by delegating +/// to the [OrderRepositoryMock] and [ExampleConnector] from the shared +/// Data Connect package. +/// +/// It follows the KROW Clean Architecture by keeping the data layer focused +/// on delegation and data mapping, without business logic. class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface { - /// Creates a [ClientCreateOrderRepositoryImpl]. /// - /// Requires an [OrderRepositoryMock] from the Data Connect shared package. + /// Requires the [OrderRepositoryMock] from the shared Data Connect package. + /// TODO: Inject and use ExampleConnector when real mutations are available. ClientCreateOrderRepositoryImpl({required OrderRepositoryMock orderMock}) - : _orderMock = orderMock; + : _orderMock = orderMock; final OrderRepositoryMock _orderMock; @override Future> getOrderTypes() { + // Delegates to Data Connect layer return _orderMock.getOrderTypes(); } @override Future createOneTimeOrder(OneTimeOrder order) { + // Delegates to Data Connect layer return _orderMock.createOneTimeOrder(order); } @override Future createRapidOrder(String description) { + // Delegates to Data Connect layer return _orderMock.createRapidOrder(description); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart index 08db06db..e2f03f83 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart @@ -2,9 +2,15 @@ import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; /// Represents the arguments required for the [CreateOneTimeOrderUseCase]. +/// +/// Encapsulates the [OneTimeOrder] details required to create a new +/// one-time staffing request. class OneTimeOrderArguments extends UseCaseArgument { - + /// Creates a [OneTimeOrderArguments] instance. + /// + /// Requires the [order] details. const OneTimeOrderArguments({required this.order}); + /// The order details to be created. final OneTimeOrder order; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart index 58212905..e6c4d95b 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart @@ -1,9 +1,15 @@ import 'package:krow_core/core.dart'; /// Represents the arguments required for the [CreateRapidOrderUseCase]. +/// +/// Encapsulates the text description of the urgent staffing need +/// for rapid order creation. class RapidOrderArguments extends UseCaseArgument { - + /// Creates a [RapidOrderArguments] instance. + /// + /// Requires the [description] of the staffing need. const RapidOrderArguments({required this.description}); + /// The text description of the urgent staffing need. final String description; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart index 895fdd64..9f2fd567 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart @@ -2,15 +2,23 @@ import 'package:krow_domain/krow_domain.dart'; /// Interface for the Client Create Order repository. /// -/// This repository handles the retrieval of available order types and the -/// submission of different types of staffing orders (Rapid, One-Time, etc.). +/// This repository is responsible for: +/// 1. Retrieving available order types for the client. +/// 2. Submitting different types of staffing orders (Rapid, One-Time). +/// +/// It follows the KROW Clean Architecture by defining the contract in the +/// domain layer, to be implemented in the data layer. abstract interface class ClientCreateOrderRepositoryInterface { - /// Retrieves the list of available order types. + /// Retrieves the list of available order types (e.g., Rapid, One-Time, Recurring). Future> getOrderTypes(); - /// Submits a one-time staffing order. + /// Submits a one-time staffing order with specific details. + /// + /// [order] contains the date, location, and required positions. Future createOneTimeOrder(OneTimeOrder order); - /// Submits a rapid (urgent) staffing order with a text description. + /// Submits a rapid (urgent) staffing order via a text description. + /// + /// [description] is the text message (or transcribed voice) describing the need. Future createRapidOrder(String description); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart index 23c92224..4f320a65 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart @@ -4,12 +4,14 @@ import '../repositories/client_create_order_repository_interface.dart'; /// Use case for creating a one-time staffing order. /// -/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit -/// a [OneTimeOrder] provided via [OneTimeOrderArguments]. +/// This use case encapsulates the logic for submitting a structured +/// staffing request and delegates the data operation to the +/// [ClientCreateOrderRepositoryInterface]. class CreateOneTimeOrderUseCase implements UseCase { - /// Creates a [CreateOneTimeOrderUseCase]. + /// + /// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer. const CreateOneTimeOrderUseCase(this._repository); final ClientCreateOrderRepositoryInterface _repository; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart index 3d2d1f0c..cf7a1459 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart @@ -4,11 +4,12 @@ import '../repositories/client_create_order_repository_interface.dart'; /// Use case for creating a rapid (urgent) staffing order. /// -/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit -/// a text-based urgent request via [RapidOrderArguments]. +/// This use case handles urgent, text-based staffing requests and +/// delegates the submission to the [ClientCreateOrderRepositoryInterface]. class CreateRapidOrderUseCase implements UseCase { - /// Creates a [CreateRapidOrderUseCase]. + /// + /// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer. const CreateRapidOrderUseCase(this._repository); final ClientCreateOrderRepositoryInterface _repository; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart index 9473369f..7fb0cc5a 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart @@ -4,11 +4,12 @@ import '../repositories/client_create_order_repository_interface.dart'; /// Use case for retrieving the available order types for a client. /// -/// This use case interacts with the [ClientCreateOrderRepositoryInterface] to -/// fetch the list of staffing order types (e.g., Rapid, One-Time). +/// This use case fetches the list of supported staffing order types +/// from the [ClientCreateOrderRepositoryInterface]. class GetOrderTypesUseCase implements NoInputUseCase> { - /// Creates a [GetOrderTypesUseCase]. + /// + /// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer. const GetOrderTypesUseCase(this._repository); final ClientCreateOrderRepositoryInterface _repository; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart index 794cdfd3..ddb2ff8e 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart @@ -7,7 +7,6 @@ import 'client_create_order_state.dart'; /// BLoC for managing the list of available order types. class ClientCreateOrderBloc extends Bloc { - ClientCreateOrderBloc(this._getOrderTypesUseCase) : super(const ClientCreateOrderInitial()) { on(_onTypesRequested); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart index 6b16d110..a3328da4 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart @@ -12,7 +12,6 @@ class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent { } class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent { - const ClientCreateOrderTypeSelected(this.typeId); final String typeId; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart index a58f89cd..5ef17693 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart @@ -16,8 +16,8 @@ class ClientCreateOrderInitial extends ClientCreateOrderState { /// State representing successfully loaded order types from the repository. class ClientCreateOrderLoadSuccess extends ClientCreateOrderState { - const ClientCreateOrderLoadSuccess(this.orderTypes); + /// The list of available order types retrieved from the domain. final List orderTypes; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart index 8d603b10..c2db55cb 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart @@ -7,7 +7,6 @@ import 'one_time_order_state.dart'; /// BLoC for managing the multi-step one-time order creation form. class OneTimeOrderBloc extends Bloc { - OneTimeOrderBloc(this._createOneTimeOrderUseCase) : super(OneTimeOrderState.initial()) { on(_onDateChanged); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart index 3574faf0..820baa04 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart @@ -6,7 +6,6 @@ import 'rapid_order_state.dart'; /// BLoC for managing the rapid (urgent) order creation flow. class RapidOrderBloc extends Bloc { - RapidOrderBloc(this._createRapidOrderUseCase) : super( const RapidOrderInitial( @@ -45,7 +44,7 @@ class RapidOrderBloc extends Bloc { // Simulate voice recognition if (newListeningState) { - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); if (state is RapidOrderInitial) { emit( (state as RapidOrderInitial).copyWith( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart index b2875f77..1c81d06f 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart @@ -8,7 +8,6 @@ abstract class RapidOrderEvent extends Equatable { } class RapidOrderMessageChanged extends RapidOrderEvent { - const RapidOrderMessageChanged(this.message); final String message; @@ -25,7 +24,6 @@ class RapidOrderSubmitted extends RapidOrderEvent { } class RapidOrderExampleSelected extends RapidOrderEvent { - const RapidOrderExampleSelected(this.example); final String example; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart index 4129ed4b..6c752b92 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart @@ -8,7 +8,6 @@ abstract class RapidOrderState extends Equatable { } class RapidOrderInitial extends RapidOrderState { - const RapidOrderInitial({ this.message = '', this.isListening = false, @@ -43,7 +42,6 @@ class RapidOrderSuccess extends RapidOrderState { } class RapidOrderFailure extends RapidOrderState { - const RapidOrderFailure(this.error); final String error; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart index 42c91202..9660439f 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart @@ -1,39 +1,15 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_domain/krow_domain.dart'; import '../blocs/client_create_order_bloc.dart'; import '../blocs/client_create_order_event.dart'; -import '../blocs/client_create_order_state.dart'; -import '../navigation/client_create_order_navigator.dart'; -import '../widgets/order_type_card.dart'; - -/// Helper to map keys to localized strings. -String _getTranslation({required String key}) { - if (key == 'client_create_order.types.rapid') { - return t.client_create_order.types.rapid; - } else if (key == 'client_create_order.types.rapid_desc') { - return t.client_create_order.types.rapid_desc; - } else if (key == 'client_create_order.types.one_time') { - return t.client_create_order.types.one_time; - } else if (key == 'client_create_order.types.one_time_desc') { - return t.client_create_order.types.one_time_desc; - } else if (key == 'client_create_order.types.recurring') { - return t.client_create_order.types.recurring; - } else if (key == 'client_create_order.types.recurring_desc') { - return t.client_create_order.types.recurring_desc; - } else if (key == 'client_create_order.types.permanent') { - return t.client_create_order.types.permanent; - } else if (key == 'client_create_order.types.permanent_desc') { - return t.client_create_order.types.permanent_desc; - } - return key; -} +import '../widgets/create_order/create_order_view.dart'; /// Main entry page for the client create order flow. -/// Allows the user to select the type of order they want to create. +/// +/// This page initializes the [ClientCreateOrderBloc] and displays the [CreateOrderView]. +/// It follows the Krow Clean Architecture by being a [StatelessWidget] and +/// delegating its state and UI to other components. class ClientCreateOrderPage extends StatelessWidget { /// Creates a [ClientCreateOrderPage]. const ClientCreateOrderPage({super.key}); @@ -43,191 +19,7 @@ class ClientCreateOrderPage extends StatelessWidget { return BlocProvider( create: (BuildContext context) => Modular.get() ..add(const ClientCreateOrderTypesRequested()), - child: const _CreateOrderView(), + child: const CreateOrderView(), ); } } - -class _CreateOrderView extends StatelessWidget { - const _CreateOrderView(); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: UiColors.bgPrimary, - appBar: UiAppBar( - title: t.client_create_order.title, - onLeadingPressed: () => Modular.to.pop(), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space5, - vertical: UiConstants.space6, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space6), - child: Text( - t.client_create_order.section_title, - style: UiTypography.footnote1m.copyWith( - color: UiColors.textDescription, - letterSpacing: 0.5, - ), - ), - ), - Expanded( - child: - BlocBuilder( - builder: - (BuildContext context, ClientCreateOrderState state) { - if (state is ClientCreateOrderLoadSuccess) { - return GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: UiConstants.space4, - crossAxisSpacing: UiConstants.space4, - childAspectRatio: 1, - ), - itemCount: state.orderTypes.length, - itemBuilder: (BuildContext context, int index) { - final OrderType type = state.orderTypes[index]; - final _OrderTypeUiMetadata ui = - _OrderTypeUiMetadata.fromId(id: type.id); - - return OrderTypeCard( - icon: ui.icon, - title: _getTranslation(key: type.titleKey), - description: _getTranslation( - key: type.descriptionKey, - ), - backgroundColor: ui.backgroundColor, - borderColor: ui.borderColor, - iconBackgroundColor: ui.iconBackgroundColor, - iconColor: ui.iconColor, - textColor: ui.textColor, - descriptionColor: ui.descriptionColor, - onTap: () { - switch (type.id) { - case 'rapid': - Modular.to.pushRapidOrder(); - break; - case 'one-time': - Modular.to.pushOneTimeOrder(); - break; - case 'recurring': - Modular.to.pushRecurringOrder(); - break; - case 'permanent': - Modular.to.pushPermanentOrder(); - break; - } - }, - ); - }, - ); - } - return const Center(child: CircularProgressIndicator()); - }, - ), - ), - ], - ), - ), - ), - ); - } -} - -/// Metadata for styling order type cards based on their ID. -class _OrderTypeUiMetadata { - const _OrderTypeUiMetadata({ - required this.icon, - required this.backgroundColor, - required this.borderColor, - required this.iconBackgroundColor, - required this.iconColor, - required this.textColor, - required this.descriptionColor, - }); - - /// Factory to get metadata based on order type ID. - factory _OrderTypeUiMetadata.fromId({required String id}) { - switch (id) { - case 'rapid': - return const _OrderTypeUiMetadata( - icon: UiIcons.zap, - backgroundColor: UiColors.tagPending, - borderColor: UiColors.separatorSpecial, - iconBackgroundColor: UiColors.textWarning, - iconColor: UiColors.white, - textColor: UiColors.textWarning, - descriptionColor: UiColors.textWarning, - ); - case 'one-time': - return const _OrderTypeUiMetadata( - icon: UiIcons.calendar, - backgroundColor: UiColors.tagInProgress, - borderColor: UiColors.primaryInverse, - iconBackgroundColor: UiColors.primary, - iconColor: UiColors.white, - textColor: UiColors.textLink, - descriptionColor: UiColors.textLink, - ); - case 'recurring': - return const _OrderTypeUiMetadata( - icon: UiIcons.rotateCcw, - backgroundColor: UiColors.tagSuccess, - borderColor: UiColors.switchActive, - iconBackgroundColor: UiColors.textSuccess, - iconColor: UiColors.white, - textColor: UiColors.textSuccess, - descriptionColor: UiColors.textSuccess, - ); - case 'permanent': - return const _OrderTypeUiMetadata( - icon: UiIcons.briefcase, - backgroundColor: UiColors.tagRefunded, - borderColor: UiColors.primaryInverse, - iconBackgroundColor: UiColors.primary, - iconColor: UiColors.white, - textColor: UiColors.textLink, - descriptionColor: UiColors.textLink, - ); - default: - return const _OrderTypeUiMetadata( - icon: UiIcons.help, - backgroundColor: UiColors.bgSecondary, - borderColor: UiColors.border, - iconBackgroundColor: UiColors.iconSecondary, - iconColor: UiColors.white, - textColor: UiColors.textPrimary, - descriptionColor: UiColors.textSecondary, - ); - } - } - - /// Icon for the order type. - final IconData icon; - - /// Background color for the card. - final Color backgroundColor; - - /// Border color for the card. - final Color borderColor; - - /// Background color for the icon. - final Color iconBackgroundColor; - - /// Color for the icon. - final Color iconColor; - - /// Color for the title text. - final Color textColor; - - /// Color for the description text. - final Color descriptionColor; -} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart index 96995b2e..a5c6202f 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart @@ -1,20 +1,15 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_domain/krow_domain.dart'; import '../blocs/one_time_order_bloc.dart'; -import '../blocs/one_time_order_event.dart'; -import '../blocs/one_time_order_state.dart'; -import '../widgets/one_time_order/one_time_order_date_picker.dart'; -import '../widgets/one_time_order/one_time_order_location_input.dart'; -import '../widgets/one_time_order/one_time_order_position_card.dart'; -import '../widgets/one_time_order/one_time_order_section_header.dart'; -import '../widgets/one_time_order/one_time_order_success_view.dart'; +import '../widgets/one_time_order/one_time_order_view.dart'; /// Page for creating a one-time staffing order. /// Users can specify the date, location, and multiple staff positions required. +/// +/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView]. +/// It follows the Krow Clean Architecture by being a [StatelessWidget] and +/// delegating its state and UI to other components. class OneTimeOrderPage extends StatelessWidget { /// Creates a [OneTimeOrderPage]. const OneTimeOrderPage({super.key}); @@ -23,186 +18,7 @@ class OneTimeOrderPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => Modular.get(), - child: const _OneTimeOrderView(), + child: const OneTimeOrderView(), ); } } - -class _OneTimeOrderView extends StatelessWidget { - const _OneTimeOrderView(); - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - - return BlocBuilder( - builder: (BuildContext context, OneTimeOrderState state) { - if (state.status == OneTimeOrderStatus.success) { - return OneTimeOrderSuccessView( - title: labels.success_title, - message: labels.success_message, - buttonLabel: 'Done', - onDone: () => Modular.to.pop(), - ); - } - - return Scaffold( - backgroundColor: UiColors.bgPrimary, - appBar: UiAppBar( - title: labels.title, - onLeadingPressed: () => Modular.to.pop(), - ), - body: Stack( - children: [ - _OneTimeOrderForm(state: state), - if (state.status == OneTimeOrderStatus.loading) - const Center(child: CircularProgressIndicator()), - ], - ), - bottomNavigationBar: _BottomActionButton( - label: labels.create_order, - isLoading: state.status == OneTimeOrderStatus.loading, - onPressed: () => BlocProvider.of(context) - .add(const OneTimeOrderSubmitted()), - ), - ); - }, - ); - } -} - -class _OneTimeOrderForm extends StatelessWidget { - const _OneTimeOrderForm({required this.state}); - final OneTimeOrderState state; - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - - return ListView( - padding: const EdgeInsets.all(UiConstants.space5), - children: [ - OneTimeOrderSectionHeader(title: labels.create_your_order), - const SizedBox(height: UiConstants.space4), - - OneTimeOrderDatePicker( - label: labels.date_label, - value: state.date, - onChanged: (DateTime date) => - BlocProvider.of(context) - .add(OneTimeOrderDateChanged(date)), - ), - const SizedBox(height: UiConstants.space4), - - OneTimeOrderLocationInput( - label: labels.location_label, - value: state.location, - onChanged: (String location) => - BlocProvider.of(context) - .add(OneTimeOrderLocationChanged(location)), - ), - const SizedBox(height: UiConstants.space6), - - OneTimeOrderSectionHeader( - title: labels.positions_title, - actionLabel: labels.add_position, - onAction: () => BlocProvider.of(context) - .add(const OneTimeOrderPositionAdded()), - ), - const SizedBox(height: UiConstants.space4), - - // Positions List - ...state.positions - .asMap() - .entries - .map((MapEntry entry) { - final int index = entry.key; - final OneTimeOrderPosition position = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space4), - child: OneTimeOrderPositionCard( - index: index, - position: position, - isRemovable: state.positions.length > 1, - positionLabel: labels.positions_title, - roleLabel: labels.select_role, - workersLabel: labels.workers_label, - startLabel: labels.start_label, - endLabel: labels.end_label, - lunchLabel: labels.lunch_break_label, - onUpdated: (OneTimeOrderPosition updated) { - BlocProvider.of(context).add( - OneTimeOrderPositionUpdated(index, updated), - ); - }, - onRemoved: () { - BlocProvider.of(context) - .add(OneTimeOrderPositionRemoved(index)); - }, - ), - ); - }), - const SizedBox(height: 100), // Space for bottom button - ], - ); - } -} - -class _BottomActionButton extends StatelessWidget { - const _BottomActionButton({ - required this.label, - required this.onPressed, - this.isLoading = false, - }); - final String label; - final VoidCallback onPressed; - final bool isLoading; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only( - left: UiConstants.space5, - right: UiConstants.space5, - top: UiConstants.space4, - bottom: MediaQuery.of(context).padding.bottom + UiConstants.space4, - ), - decoration: BoxDecoration( - color: UiColors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, -4), - ), - ], - ), - child: isLoading - ? const UiButton( - buttonBuilder: _dummyBuilder, - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - color: UiColors.primary, strokeWidth: 2), - ), - ) - : UiButton.primary( - text: label, - onPressed: onPressed, - size: UiButtonSize.large, - ), - ); - } - - static Widget _dummyBuilder( - BuildContext context, - VoidCallback? onPressed, - ButtonStyle? style, - Widget child, - ) { - return Center(child: child); - } -} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart index 0f0bb874..2bb444cf 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart @@ -1,18 +1,15 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:intl/intl.dart'; import '../blocs/rapid_order_bloc.dart'; -import '../blocs/rapid_order_event.dart'; -import '../blocs/rapid_order_state.dart'; -import '../widgets/rapid_order/rapid_order_example_card.dart'; -import '../widgets/rapid_order/rapid_order_header.dart'; -import '../widgets/rapid_order/rapid_order_success_view.dart'; +import '../widgets/rapid_order/rapid_order_view.dart'; /// Rapid Order Flow Page - Emergency staffing requests. /// Features voice recognition simulation and quick example selection. +/// +/// This page initializes the [RapidOrderBloc] and displays the [RapidOrderView]. +/// It follows the Krow Clean Architecture by being a [StatelessWidget] and +/// delegating its state and UI to other components. class RapidOrderPage extends StatelessWidget { /// Creates a [RapidOrderPage]. const RapidOrderPage({super.key}); @@ -21,306 +18,7 @@ class RapidOrderPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => Modular.get(), - child: const _RapidOrderView(), - ); - } -} - -class _RapidOrderView extends StatelessWidget { - const _RapidOrderView(); - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderRapidEn labels = - t.client_create_order.rapid; - - return BlocBuilder( - builder: (BuildContext context, RapidOrderState state) { - if (state is RapidOrderSuccess) { - return RapidOrderSuccessView( - title: labels.success_title, - message: labels.success_message, - buttonLabel: labels.back_to_orders, - onDone: () => Modular.to.pop(), - ); - } - - return const _RapidOrderForm(); - }, - ); - } -} - -class _RapidOrderForm extends StatefulWidget { - const _RapidOrderForm(); - - @override - State<_RapidOrderForm> createState() => _RapidOrderFormState(); -} - -class _RapidOrderFormState extends State<_RapidOrderForm> { - final TextEditingController _messageController = TextEditingController(); - - @override - void dispose() { - _messageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderRapidEn labels = - t.client_create_order.rapid; - final DateTime now = DateTime.now(); - final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now); - final String timeStr = DateFormat('h:mm a').format(now); - - return BlocListener( - listener: (BuildContext context, RapidOrderState state) { - if (state is RapidOrderInitial) { - if (_messageController.text != state.message) { - _messageController.text = state.message; - _messageController.selection = TextSelection.fromPosition( - TextPosition(offset: _messageController.text.length), - ); - } - } - }, - child: Scaffold( - backgroundColor: UiColors.bgPrimary, - body: Column( - children: [ - RapidOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - date: dateStr, - time: timeStr, - onBack: () => Modular.to.pop(), - ), - - // Content - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - labels.tell_us, - style: UiTypography.headline3m.textPrimary, - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: UiConstants.space1, - ), - decoration: BoxDecoration( - color: UiColors.destructive, - borderRadius: UiConstants.radiusSm, - ), - child: Text( - labels.urgent_badge, - style: UiTypography.footnote2b.copyWith( - color: UiColors.white, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), - - // Main Card - Container( - padding: const EdgeInsets.all(UiConstants.space6), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: BlocBuilder( - builder: (BuildContext context, RapidOrderState state) { - final RapidOrderInitial? initialState = - state is RapidOrderInitial ? state : null; - final bool isSubmitting = - state is RapidOrderSubmitting; - - return Column( - children: [ - // Icon - _AnimatedZapIcon(), - const SizedBox(height: UiConstants.space4), - Text( - labels.need_staff, - style: UiTypography.headline2m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - Text( - labels.type_or_speak, - textAlign: TextAlign.center, - style: UiTypography.body2r.textSecondary, - ), - const SizedBox(height: UiConstants.space6), - - // Examples - if (initialState != null) - ...initialState.examples - .asMap() - .entries - .map((MapEntry entry) { - final int index = entry.key; - final String example = entry.value; - final bool isHighlighted = index == 0; - - return Padding( - padding: const EdgeInsets.only( - bottom: UiConstants.space2), - child: RapidOrderExampleCard( - example: example, - isHighlighted: isHighlighted, - label: labels.example, - onTap: () => - BlocProvider.of( - context) - .add( - RapidOrderExampleSelected(example), - ), - ), - ); - }), - const SizedBox(height: UiConstants.space4), - - // Input - TextField( - controller: _messageController, - maxLines: 4, - onChanged: (String value) { - BlocProvider.of(context).add( - RapidOrderMessageChanged(value), - ); - }, - decoration: InputDecoration( - hintText: labels.hint, - hintStyle: UiTypography.body2r.copyWith( - color: UiColors.textPlaceholder, - ), - border: OutlineInputBorder( - borderRadius: UiConstants.radiusLg, - borderSide: const BorderSide( - color: UiColors.border, - ), - ), - contentPadding: - const EdgeInsets.all(UiConstants.space4), - ), - ), - const SizedBox(height: UiConstants.space4), - - // Actions - _RapidOrderActions( - labels: labels, - isSubmitting: isSubmitting, - isListening: initialState?.isListening ?? false, - isMessageEmpty: initialState != null && - initialState.message.trim().isEmpty, - ), - ], - ); - }, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} - -class _AnimatedZapIcon extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Container( - width: 64, - height: 64, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - UiColors.destructive, - UiColors.destructive.withValues(alpha: 0.85), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: UiConstants.radiusLg, - boxShadow: [ - BoxShadow( - color: UiColors.destructive.withValues(alpha: 0.3), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: const Icon( - UiIcons.zap, - color: UiColors.white, - size: 32, - ), - ); - } -} - -class _RapidOrderActions extends StatelessWidget { - const _RapidOrderActions({ - required this.labels, - required this.isSubmitting, - required this.isListening, - required this.isMessageEmpty, - }); - final TranslationsClientCreateOrderRapidEn labels; - final bool isSubmitting; - final bool isListening; - final bool isMessageEmpty; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: UiButton.secondary( - text: isListening ? labels.listening : labels.speak, - leadingIcon: UiIcons.bell, // Placeholder for mic - onPressed: () => BlocProvider.of(context).add( - const RapidOrderVoiceToggled(), - ), - style: OutlinedButton.styleFrom( - backgroundColor: isListening - ? UiColors.destructive.withValues(alpha: 0.05) - : null, - side: isListening - ? const BorderSide(color: UiColors.destructive) - : null, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: UiButton.primary( - text: isSubmitting ? labels.sending : labels.send, - trailingIcon: UiIcons.arrowRight, - onPressed: isSubmitting || isMessageEmpty - ? null - : () => BlocProvider.of(context).add( - const RapidOrderSubmitted(), - ), - ), - ), - ], + child: const RapidOrderView(), ); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/ui_entities/order_type_ui_metadata.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/ui_entities/order_type_ui_metadata.dart new file mode 100644 index 00000000..0729f4a1 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/ui_entities/order_type_ui_metadata.dart @@ -0,0 +1,93 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/widgets.dart'; + +/// Metadata for styling order type cards based on their ID. +class OrderTypeUiMetadata { + /// Creates an [OrderTypeUiMetadata]. + const OrderTypeUiMetadata({ + required this.icon, + required this.backgroundColor, + required this.borderColor, + required this.iconBackgroundColor, + required this.iconColor, + required this.textColor, + required this.descriptionColor, + }); + + /// Factory to get metadata based on order type ID. + factory OrderTypeUiMetadata.fromId({required String id}) { + switch (id) { + case 'rapid': + return const OrderTypeUiMetadata( + icon: UiIcons.zap, + backgroundColor: UiColors.tagPending, + borderColor: UiColors.separatorSpecial, + iconBackgroundColor: UiColors.textWarning, + iconColor: UiColors.white, + textColor: UiColors.textWarning, + descriptionColor: UiColors.textWarning, + ); + case 'one-time': + return const OrderTypeUiMetadata( + icon: UiIcons.calendar, + backgroundColor: UiColors.tagInProgress, + borderColor: UiColors.primaryInverse, + iconBackgroundColor: UiColors.primary, + iconColor: UiColors.white, + textColor: UiColors.textLink, + descriptionColor: UiColors.textLink, + ); + case 'recurring': + return const OrderTypeUiMetadata( + icon: UiIcons.rotateCcw, + backgroundColor: UiColors.tagSuccess, + borderColor: UiColors.switchActive, + iconBackgroundColor: UiColors.textSuccess, + iconColor: UiColors.white, + textColor: UiColors.textSuccess, + descriptionColor: UiColors.textSuccess, + ); + case 'permanent': + return const OrderTypeUiMetadata( + icon: UiIcons.briefcase, + backgroundColor: UiColors.tagRefunded, + borderColor: UiColors.primaryInverse, + iconBackgroundColor: UiColors.primary, + iconColor: UiColors.white, + textColor: UiColors.textLink, + descriptionColor: UiColors.textLink, + ); + default: + return const OrderTypeUiMetadata( + icon: UiIcons.help, + backgroundColor: UiColors.bgSecondary, + borderColor: UiColors.border, + iconBackgroundColor: UiColors.iconSecondary, + iconColor: UiColors.white, + textColor: UiColors.textPrimary, + descriptionColor: UiColors.textSecondary, + ); + } + } + + /// Icon for the order type. + final IconData icon; + + /// Background color for the card. + final Color backgroundColor; + + /// Border color for the card. + final Color borderColor; + + /// Background color for the icon. + final Color iconBackgroundColor; + + /// Color for the icon. + final Color iconColor; + + /// Color for the title text. + final Color textColor; + + /// Color for the description text. + final Color descriptionColor; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart new file mode 100644 index 00000000..bc007565 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart @@ -0,0 +1,129 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../blocs/client_create_order_bloc.dart'; +import '../../blocs/client_create_order_state.dart'; +import '../../navigation/client_create_order_navigator.dart'; +import '../../ui_entities/order_type_ui_metadata.dart'; +import '../order_type_card.dart'; + +/// Helper to map keys to localized strings. +String _getTranslation({required String key}) { + if (key == 'client_create_order.types.rapid') { + return t.client_create_order.types.rapid; + } else if (key == 'client_create_order.types.rapid_desc') { + return t.client_create_order.types.rapid_desc; + } else if (key == 'client_create_order.types.one_time') { + return t.client_create_order.types.one_time; + } else if (key == 'client_create_order.types.one_time_desc') { + return t.client_create_order.types.one_time_desc; + } else if (key == 'client_create_order.types.recurring') { + return t.client_create_order.types.recurring; + } else if (key == 'client_create_order.types.recurring_desc') { + return t.client_create_order.types.recurring_desc; + } else if (key == 'client_create_order.types.permanent') { + return t.client_create_order.types.permanent; + } else if (key == 'client_create_order.types.permanent_desc') { + return t.client_create_order.types.permanent_desc; + } + return key; +} + +/// The main content of the Create Order page. +class CreateOrderView extends StatelessWidget { + /// Creates a [CreateOrderView]. + const CreateOrderView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: t.client_create_order.title, + onLeadingPressed: () => Modular.to.pop(), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space6, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space6), + child: Text( + t.client_create_order.section_title, + style: UiTypography.footnote1m.copyWith( + color: UiColors.textDescription, + letterSpacing: 0.5, + ), + ), + ), + Expanded( + child: + BlocBuilder( + builder: + (BuildContext context, ClientCreateOrderState state) { + if (state is ClientCreateOrderLoadSuccess) { + return GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: UiConstants.space4, + crossAxisSpacing: UiConstants.space4, + childAspectRatio: 1, + ), + itemCount: state.orderTypes.length, + itemBuilder: (BuildContext context, int index) { + final OrderType type = state.orderTypes[index]; + final OrderTypeUiMetadata ui = + OrderTypeUiMetadata.fromId(id: type.id); + + return OrderTypeCard( + icon: ui.icon, + title: _getTranslation(key: type.titleKey), + description: _getTranslation( + key: type.descriptionKey, + ), + backgroundColor: ui.backgroundColor, + borderColor: ui.borderColor, + iconBackgroundColor: ui.iconBackgroundColor, + iconColor: ui.iconColor, + textColor: ui.textColor, + descriptionColor: ui.descriptionColor, + onTap: () { + switch (type.id) { + case 'rapid': + Modular.to.pushRapidOrder(); + break; + case 'one-time': + Modular.to.pushOneTimeOrder(); + break; + case 'recurring': + Modular.to.pushRecurringOrder(); + break; + case 'permanent': + Modular.to.pushPermanentOrder(); + break; + } + }, + ); + }, + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart index 5b32274d..5a0eb751 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart @@ -3,7 +3,16 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; /// A date picker field for the one-time order form. -class OneTimeOrderDatePicker extends StatelessWidget { +/// Matches the prototype input field style. +class OneTimeOrderDatePicker extends StatefulWidget { + /// Creates a [OneTimeOrderDatePicker]. + const OneTimeOrderDatePicker({ + required this.label, + required this.value, + required this.onChanged, + super.key, + }); + /// The label text to display above the field. final String label; @@ -13,56 +22,53 @@ class OneTimeOrderDatePicker extends StatelessWidget { /// Callback when a new date is selected. final ValueChanged onChanged; - /// Creates a [OneTimeOrderDatePicker]. - const OneTimeOrderDatePicker({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); + @override + State createState() => _OneTimeOrderDatePickerState(); +} + +class _OneTimeOrderDatePickerState extends State { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController( + text: DateFormat('yyyy-MM-dd').format(widget.value), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(OneTimeOrderDatePicker oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != oldWidget.value) { + _controller.text = DateFormat('yyyy-MM-dd').format(widget.value); + } + } @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote1m.textSecondary), - const SizedBox(height: UiConstants.space2), - InkWell( - onTap: () async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: value, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) { - onChanged(picked); - } - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3 + 2, - ), - decoration: BoxDecoration( - border: Border.all(color: UiColors.border), - borderRadius: UiConstants.radiusLg, - ), - child: Row( - children: [ - const Icon(UiIcons.calendar, - size: 20, color: UiColors.iconSecondary), - const SizedBox(width: UiConstants.space3), - Text( - DateFormat('EEEE, MMM d, yyyy').format(value), - style: UiTypography.body1r.textPrimary, - ), - ], - ), - ), - ), - ], + return UiTextField( + label: widget.label, + controller: _controller, + readOnly: true, + prefixIcon: UiIcons.calendar, + onTap: () async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: widget.value, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null) { + widget.onChanged(picked); + } + }, ); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart new file mode 100644 index 00000000..3dbf2a38 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart @@ -0,0 +1,73 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A header widget for the one-time order flow with a colored background. +class OneTimeOrderHeader extends StatelessWidget { + /// Creates a [OneTimeOrderHeader]. + const OneTimeOrderHeader({ + required this.title, + required this.subtitle, + required this.onBack, + super.key, + }); + + /// The title of the page. + final String title; + + /// The subtitle or description. + final String subtitle; + + /// Callback when the back button is pressed. + final VoidCallback onBack; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + UiConstants.space5, + bottom: UiConstants.space5, + left: UiConstants.space5, + right: UiConstants.space5, + ), + color: UiColors.primary, + child: Row( + children: [ + GestureDetector( + onTap: onBack, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.2), + borderRadius: UiConstants.radiusMd, + ), + child: const Icon( + UiIcons.chevronLeft, + color: UiColors.white, + size: 24, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: UiTypography.headline3m.copyWith( + color: UiColors.white, + ), + ), + Text( + subtitle, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart index 3f93da9d..7eb8baf1 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart @@ -2,16 +2,8 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; /// A location input field for the one-time order form. -class OneTimeOrderLocationInput extends StatelessWidget { - /// The label text to display above the field. - final String label; - - /// The current location value. - final String value; - - /// Callback when the location text changes. - final ValueChanged onChanged; - +/// Matches the prototype input field style. +class OneTimeOrderLocationInput extends StatefulWidget { /// Creates a [OneTimeOrderLocationInput]. const OneTimeOrderLocationInput({ required this.label, @@ -20,14 +12,50 @@ class OneTimeOrderLocationInput extends StatelessWidget { super.key, }); + /// The label text to display above the field. + final String label; + + /// The current location value. + final String value; + + /// Callback when the location value changes. + final ValueChanged onChanged; + + @override + State createState() => + _OneTimeOrderLocationInputState(); +} + +class _OneTimeOrderLocationInputState extends State { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.value); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(OneTimeOrderLocationInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != _controller.text) { + _controller.text = widget.value; + } + } + @override Widget build(BuildContext context) { return UiTextField( - label: label, - hintText: 'Select Branch/Location', - controller: TextEditingController(text: value) - ..selection = TextSelection.collapsed(offset: value.length), - onChanged: onChanged, + label: widget.label, + controller: _controller, + onChanged: widget.onChanged, + hintText: 'Enter address', prefixIcon: UiIcons.mapPin, ); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart index a605ea5c..4b24cdfb 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart @@ -1,9 +1,27 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:krow_domain/krow_domain.dart'; /// A card widget for editing a specific position in a one-time order. +/// Matches the prototype layout while using design system tokens. class OneTimeOrderPositionCard extends StatelessWidget { + /// Creates a [OneTimeOrderPositionCard]. + const OneTimeOrderPositionCard({ + required this.index, + required this.position, + required this.isRemovable, + required this.onUpdated, + required this.onRemoved, + required this.positionLabel, + required this.roleLabel, + required this.workersLabel, + required this.startLabel, + required this.endLabel, + required this.lunchLabel, + super.key, + }); + /// The index of the position in the list. final int index; @@ -37,22 +55,6 @@ class OneTimeOrderPositionCard extends StatelessWidget { /// Label for the lunch break. final String lunchLabel; - /// Creates a [OneTimeOrderPositionCard]. - const OneTimeOrderPositionCard({ - required this.index, - required this.position, - required this.isRemovable, - required this.onUpdated, - required this.onRemoved, - required this.positionLabel, - required this.roleLabel, - required this.workersLabel, - required this.startLabel, - required this.endLabel, - required this.lunchLabel, - super.key, - }); - @override Widget build(BuildContext context) { return Container( @@ -61,13 +63,6 @@ class OneTimeOrderPositionCard extends StatelessWidget { color: UiColors.white, borderRadius: UiConstants.radiusLg, border: Border.all(color: UiColors.border), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -77,149 +72,281 @@ class OneTimeOrderPositionCard extends StatelessWidget { children: [ Text( '$positionLabel #${index + 1}', - style: UiTypography.body1b.textPrimary, + style: UiTypography.footnote1m.textSecondary, ), if (isRemovable) - IconButton( - icon: const Icon(UiIcons.delete, - size: 20, color: UiColors.destructive), - onPressed: onRemoved, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - visualDensity: VisualDensity.compact, + GestureDetector( + onTap: onRemoved, + child: Text( + t.client_create_order.one_time.remove, + style: UiTypography.footnote1m.copyWith( + color: UiColors.destructive, + ), + ), ), ], ), - const Divider(height: UiConstants.space6), + const SizedBox(height: UiConstants.space3), // Role (Dropdown) - _LabelField( - label: roleLabel, - child: DropdownButtonFormField( - value: position.role.isEmpty ? null : position.role, - items: ['Server', 'Bartender', 'Cook', 'Busser', 'Host'] - .map((String role) => DropdownMenuItem( - value: role, - child: - Text(role, style: UiTypography.body1r.textPrimary), - )) - .toList(), - onChanged: (String? val) { - if (val != null) { - onUpdated(position.copyWith(role: val)); - } - }, - decoration: _inputDecoration(UiIcons.briefcase), + Container( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + height: 44, + decoration: BoxDecoration( + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + hint: + Text(roleLabel, style: UiTypography.body2r.textPlaceholder), + value: position.role.isEmpty ? null : position.role, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (String? val) { + if (val != null) { + onUpdated(position.copyWith(role: val)); + } + }, + items: [ + 'Server', + 'Bartender', + 'Cook', + 'Busser', + 'Host', + 'Barista', + 'Dishwasher', + 'Event Staff' + ].map((String role) { + // Mock rates for UI matching + final int rate = _getMockRate(role); + return DropdownMenuItem( + value: role, + child: Text( + '$role - \$$rate/hr', + style: UiTypography.body2r.textPrimary, + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space3), + + // Start/End/Workers Row + Row( + children: [ + // Start Time + Expanded( + child: _buildTimeInput( + context: context, + label: startLabel, + value: position.startTime, + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (picked != null && context.mounted) { + onUpdated( + position.copyWith(startTime: picked.format(context))); + } + }, + ), + ), + const SizedBox(width: UiConstants.space2), + // End Time + Expanded( + child: _buildTimeInput( + context: context, + label: endLabel, + value: position.endTime, + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (picked != null && context.mounted) { + onUpdated( + position.copyWith(endTime: picked.format(context))); + } + }, + ), + ), + const SizedBox(width: UiConstants.space2), + // Workers Count + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + workersLabel, + style: UiTypography.footnote2r.textSecondary, + ), + const SizedBox(height: UiConstants.space1), + Container( + height: 40, + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusSm, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () => onUpdated(position.copyWith( + count: (position.count > 1) + ? position.count - 1 + : 1)), + child: const Icon(UiIcons.minus, size: 12), + ), + Text( + '${position.count}', + style: UiTypography.body2b.textPrimary, + ), + GestureDetector( + onTap: () => onUpdated( + position.copyWith(count: position.count + 1)), + child: const Icon(UiIcons.add, size: 12), + ), + ], + ), + ), + ], + ), + ), + ], ), const SizedBox(height: UiConstants.space4), - // Count (Counter) - _LabelField( - label: workersLabel, - child: Row( + // Optional Location Override + if (position.location == null) + GestureDetector( + onTap: () => onUpdated(position.copyWith(location: '')), + child: Row( + children: [ + const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary), + const SizedBox(width: UiConstants.space1), + Text( + t.client_create_order.one_time.different_location, + style: UiTypography.footnote1m.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _CounterButton( - icon: UiIcons.minus, - onPressed: position.count > 1 - ? () => onUpdated( - position.copyWith(count: position.count - 1)) - : null, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(UiIcons.mapPin, + size: 14, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space1), + Text( + t.client_create_order.one_time + .different_location_title, + style: UiTypography.footnote1m.textSecondary, + ), + ], + ), + GestureDetector( + onTap: () => onUpdated(position.copyWith(location: null)), + child: const Icon( + UiIcons.close, + size: 14, + color: UiColors.destructive, + ), + ), + ], ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4), - child: Text('${position.count}', - style: UiTypography.headline3m.textPrimary), - ), - _CounterButton( - icon: UiIcons.add, - onPressed: () => - onUpdated(position.copyWith(count: position.count + 1)), + const SizedBox(height: UiConstants.space2), + _PositionLocationInput( + value: position.location ?? '', + onChanged: (String val) => + onUpdated(position.copyWith(location: val)), + hintText: + t.client_create_order.one_time.different_location_hint, ), ], ), - ), - const SizedBox(height: UiConstants.space4), - // Start/End Time - Row( - children: [ - Expanded( - child: _LabelField( - label: startLabel, - child: InkWell( - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: const TimeOfDay(hour: 9, minute: 0), - ); - if (picked != null) { - onUpdated(position.copyWith( - startTime: picked.format(context))); - } - }, - child: Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: _boxDecoration(), - child: Text( - position.startTime.isEmpty - ? '--:--' - : position.startTime, - style: UiTypography.body1r.textPrimary, - ), - ), - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: _LabelField( - label: endLabel, - child: InkWell( - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: const TimeOfDay(hour: 17, minute: 0), - ); - if (picked != null) { - onUpdated( - position.copyWith(endTime: picked.format(context))); - } - }, - child: Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: _boxDecoration(), - child: Text( - position.endTime.isEmpty ? '--:--' : position.endTime, - style: UiTypography.body1r.textPrimary, - ), - ), - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), + const SizedBox(height: UiConstants.space3), // Lunch Break - _LabelField( - label: lunchLabel, - child: DropdownButtonFormField( - value: position.lunchBreak, - items: [0, 30, 45, 60] - .map((int mins) => DropdownMenuItem( - value: mins, - child: Text('${mins}m', - style: UiTypography.body1r.textPrimary), - )) - .toList(), - onChanged: (int? val) { - if (val != null) { - onUpdated(position.copyWith(lunchBreak: val)); - } - }, - decoration: _inputDecoration(UiIcons.clock), + Text( + lunchLabel, + style: UiTypography.footnote2r.textSecondary, + ), + const SizedBox(height: UiConstants.space1), + Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: position.lunchBreak, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (int? val) { + if (val != null) { + onUpdated(position.copyWith(lunchBreak: val)); + } + }, + items: >[ + DropdownMenuItem( + value: 0, + child: Text(t.client_create_order.one_time.no_break, + style: UiTypography.body2r.textPrimary), + ), + DropdownMenuItem( + value: 10, + child: Text( + '10 ${t.client_create_order.one_time.paid_break}', + style: UiTypography.body2r.textPrimary), + ), + DropdownMenuItem( + value: 15, + child: Text( + '15 ${t.client_create_order.one_time.paid_break}', + style: UiTypography.body2r.textPrimary), + ), + DropdownMenuItem( + value: 30, + child: Text( + '30 ${t.client_create_order.one_time.unpaid_break}', + style: UiTypography.body2r.textPrimary), + ), + DropdownMenuItem( + value: 45, + child: Text( + '45 ${t.client_create_order.one_time.unpaid_break}', + style: UiTypography.body2r.textPrimary), + ), + DropdownMenuItem( + value: 60, + child: Text( + '60 ${t.client_create_order.one_time.unpaid_break}', + style: UiTypography.body2r.textPrimary), + ), + ], + ), ), ), ], @@ -227,68 +354,89 @@ class OneTimeOrderPositionCard extends StatelessWidget { ); } - InputDecoration _inputDecoration(IconData icon) => InputDecoration( - prefixIcon: Icon(icon, size: 18, color: UiColors.iconSecondary), - contentPadding: - const EdgeInsets.symmetric(horizontal: UiConstants.space3), - border: OutlineInputBorder( - borderRadius: UiConstants.radiusLg, - borderSide: const BorderSide(color: UiColors.border), - ), - ); - - BoxDecoration _boxDecoration() => BoxDecoration( - border: Border.all(color: UiColors.border), - borderRadius: UiConstants.radiusLg, - ); -} - -class _LabelField extends StatelessWidget { - const _LabelField({required this.label, required this.child}); - final String label; - final Widget child; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote1m.textSecondary), - const SizedBox(height: UiConstants.space1), - child, - ], + Widget _buildTimeInput({ + required BuildContext context, + required String label, + required String value, + required VoidCallback onTap, + }) { + return UiTextField( + label: label, + controller: TextEditingController(text: value), + readOnly: true, + onTap: onTap, + hintText: '--:--', ); } + + int _getMockRate(String role) { + switch (role) { + case 'Server': + return 18; + case 'Bartender': + return 22; + case 'Cook': + return 20; + case 'Busser': + return 16; + case 'Host': + return 17; + case 'Barista': + return 16; + case 'Dishwasher': + return 15; + case 'Event Staff': + return 20; + default: + return 15; + } + } } -class _CounterButton extends StatelessWidget { - const _CounterButton({required this.icon, this.onPressed}); - final IconData icon; - final VoidCallback? onPressed; +class _PositionLocationInput extends StatefulWidget { + const _PositionLocationInput({ + required this.value, + required this.hintText, + required this.onChanged, + }); + + final String value; + final String hintText; + final ValueChanged onChanged; + + @override + State<_PositionLocationInput> createState() => _PositionLocationInputState(); +} + +class _PositionLocationInputState extends State<_PositionLocationInput> { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.value); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(_PositionLocationInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != _controller.text) { + _controller.text = widget.value; + } + } @override Widget build(BuildContext context) { - return InkWell( - onTap: onPressed, - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - border: Border.all( - color: onPressed != null - ? UiColors.border - : UiColors.border.withOpacity(0.5)), - borderRadius: UiConstants.radiusLg, - color: onPressed != null ? UiColors.white : UiColors.background, - ), - child: Icon( - icon, - size: 16, - color: onPressed != null - ? UiColors.iconPrimary - : UiColors.iconSecondary.withOpacity(0.5), - ), - ), + return UiTextField( + controller: _controller, + onChanged: widget.onChanged, + hintText: widget.hintText, ); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart index 29c8df31..61adb94a 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart @@ -3,6 +3,14 @@ import 'package:flutter/material.dart'; /// A header widget for sections in the one-time order form. class OneTimeOrderSectionHeader extends StatelessWidget { + /// Creates a [OneTimeOrderSectionHeader]. + const OneTimeOrderSectionHeader({ + required this.title, + this.actionLabel, + this.onAction, + super.key, + }); + /// The title text for the section. final String title; @@ -12,14 +20,6 @@ class OneTimeOrderSectionHeader extends StatelessWidget { /// Callback when the action button is tapped. final VoidCallback? onAction; - /// Creates a [OneTimeOrderSectionHeader]. - const OneTimeOrderSectionHeader({ - required this.title, - this.actionLabel, - this.onAction, - super.key, - }); - @override Widget build(BuildContext context) { return Row( @@ -27,13 +27,14 @@ class OneTimeOrderSectionHeader extends StatelessWidget { children: [ Text(title, style: UiTypography.headline4m.textPrimary), if (actionLabel != null && onAction != null) - TextButton.icon( + UiButton.text( onPressed: onAction, - icon: const Icon(UiIcons.add, size: 16, color: UiColors.primary), - label: Text(actionLabel!, style: UiTypography.body2b.textPrimary), + leadingIcon: UiIcons.add, + text: actionLabel!, + iconSize: 16, style: TextButton.styleFrom( - padding: - const EdgeInsets.symmetric(horizontal: UiConstants.space2), + minimumSize: const Size(0, 24), + maximumSize: const Size(0, 24), ), ), ], diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart index ea704758..3a660a86 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart @@ -2,7 +2,17 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; /// A view to display when a one-time order has been successfully created. +/// Matches the prototype success view layout with a gradient background and centered card. class OneTimeOrderSuccessView extends StatelessWidget { + /// Creates a [OneTimeOrderSuccessView]. + const OneTimeOrderSuccessView({ + required this.title, + required this.message, + required this.buttonLabel, + required this.onDone, + super.key, + }); + /// The title of the success message. final String title; @@ -15,54 +25,78 @@ class OneTimeOrderSuccessView extends StatelessWidget { /// Callback when the completion button is tapped. final VoidCallback onDone; - /// Creates a [OneTimeOrderSuccessView]. - const OneTimeOrderSuccessView({ - required this.title, - required this.message, - required this.buttonLabel, - required this.onDone, - super.key, - }); - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: UiColors.white, - body: Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 100, - height: 100, - decoration: const BoxDecoration( - color: UiColors.tagSuccess, - shape: BoxShape.circle, - ), - child: const Icon(UiIcons.check, - size: 50, color: UiColors.textSuccess), + body: Container( + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [UiColors.primary, UiColors.buttonPrimaryHover], + ), + ), + child: SafeArea( + child: Center( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 40), + padding: const EdgeInsets.all(UiConstants.space8), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg * 1.5, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], ), - const SizedBox(height: UiConstants.space8), - Text( - title, - style: UiTypography.headline2m.textPrimary, - textAlign: TextAlign.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, + height: 64, + decoration: const BoxDecoration( + color: UiColors.accent, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + UiIcons.check, + color: UiColors.black, + size: 32, + ), + ), + ), + const SizedBox(height: UiConstants.space6), + Text( + title, + style: UiTypography.headline2m.textPrimary, + textAlign: TextAlign.center, + ), + const SizedBox(height: UiConstants.space3), + Text( + message, + textAlign: TextAlign.center, + style: UiTypography.body2r.textSecondary.copyWith( + height: 1.5, + ), + ), + const SizedBox(height: UiConstants.space8), + SizedBox( + width: double.infinity, + child: UiButton.primary( + text: buttonLabel, + onPressed: onDone, + size: UiButtonSize.large, + ), + ), + ], ), - const SizedBox(height: UiConstants.space4), - Text( - message, - style: UiTypography.body1r.textSecondary, - textAlign: TextAlign.center, - ), - const SizedBox(height: UiConstants.space10), - UiButton.primary( - text: buttonLabel, - onPressed: onDone, - size: UiButtonSize.large, - ), - ], + ), ), ), ), diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart new file mode 100644 index 00000000..404cbb56 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart @@ -0,0 +1,185 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../blocs/one_time_order_bloc.dart'; +import '../../blocs/one_time_order_event.dart'; +import '../../blocs/one_time_order_state.dart'; +import 'one_time_order_date_picker.dart'; +import 'one_time_order_header.dart'; +import 'one_time_order_location_input.dart'; +import 'one_time_order_position_card.dart'; +import 'one_time_order_section_header.dart'; +import 'one_time_order_success_view.dart'; + +/// The main content of the One-Time Order page. +class OneTimeOrderView extends StatelessWidget { + /// Creates a [OneTimeOrderView]. + const OneTimeOrderView({super.key}); + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + + return BlocBuilder( + builder: (BuildContext context, OneTimeOrderState state) { + if (state.status == OneTimeOrderStatus.success) { + return OneTimeOrderSuccessView( + title: labels.success_title, + message: labels.success_message, + buttonLabel: labels.back_to_orders, + onDone: () => Modular.to.pop(), + ); + } + + return Scaffold( + backgroundColor: UiColors.bgPrimary, + body: Column( + children: [ + OneTimeOrderHeader( + title: labels.title, + subtitle: labels.subtitle, + onBack: () => Modular.to.pop(), + ), + Expanded( + child: Stack( + children: [ + _OneTimeOrderForm(state: state), + if (state.status == OneTimeOrderStatus.loading) + const Center(child: CircularProgressIndicator()), + ], + ), + ), + _BottomActionButton( + label: state.status == OneTimeOrderStatus.loading + ? labels.creating + : labels.create_order, + isLoading: state.status == OneTimeOrderStatus.loading, + onPressed: () => BlocProvider.of(context) + .add(const OneTimeOrderSubmitted()), + ), + ], + ), + ); + }, + ); + } +} + +class _OneTimeOrderForm extends StatelessWidget { + const _OneTimeOrderForm({required this.state}); + final OneTimeOrderState state; + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + + return ListView( + padding: const EdgeInsets.all(UiConstants.space5), + children: [ + Text( + labels.create_your_order, + style: UiTypography.headline3m.textPrimary, + ), + const SizedBox(height: UiConstants.space4), + + OneTimeOrderDatePicker( + label: labels.date_label, + value: state.date, + onChanged: (DateTime date) => + BlocProvider.of(context) + .add(OneTimeOrderDateChanged(date)), + ), + const SizedBox(height: UiConstants.space4), + + OneTimeOrderLocationInput( + label: labels.location_label, + value: state.location, + onChanged: (String location) => + BlocProvider.of(context) + .add(OneTimeOrderLocationChanged(location)), + ), + const SizedBox(height: UiConstants.space6), + + OneTimeOrderSectionHeader( + title: labels.positions_title, + actionLabel: labels.add_position, + onAction: () => BlocProvider.of(context) + .add(const OneTimeOrderPositionAdded()), + ), + const SizedBox(height: UiConstants.space3), + + // Positions List + ...state.positions + .asMap() + .entries + .map((MapEntry entry) { + final int index = entry.key; + final OneTimeOrderPosition position = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: OneTimeOrderPositionCard( + index: index, + position: position, + isRemovable: state.positions.length > 1, + positionLabel: labels.positions_title, + roleLabel: labels.select_role, + workersLabel: labels.workers_label, + startLabel: labels.start_label, + endLabel: labels.end_label, + lunchLabel: labels.lunch_break_label, + onUpdated: (OneTimeOrderPosition updated) { + BlocProvider.of(context).add( + OneTimeOrderPositionUpdated(index, updated), + ); + }, + onRemoved: () { + BlocProvider.of(context) + .add(OneTimeOrderPositionRemoved(index)); + }, + ), + ); + }), + ], + ); + } +} + +class _BottomActionButton extends StatelessWidget { + const _BottomActionButton({ + required this.label, + required this.onPressed, + this.isLoading = false, + }); + final String label; + final VoidCallback onPressed; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only( + left: UiConstants.space5, + right: UiConstants.space5, + top: UiConstants.space5, + bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5, + ), + decoration: const BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.border)), + ), + child: SizedBox( + width: double.infinity, + child: UiButton.primary( + text: label, + onPressed: isLoading ? null : onPressed, + size: UiButtonSize.large, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart index 8b450b99..9a6a4535 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart @@ -3,6 +3,21 @@ import 'package:flutter/material.dart'; /// A card widget representing an order type in the creation flow. class OrderTypeCard extends StatelessWidget { + /// Creates an [OrderTypeCard]. + const OrderTypeCard({ + required this.icon, + required this.title, + required this.description, + required this.backgroundColor, + required this.borderColor, + required this.iconBackgroundColor, + required this.iconColor, + required this.textColor, + required this.descriptionColor, + required this.onTap, + super.key, + }); + /// Icon to display at the top of the card. final IconData icon; @@ -33,21 +48,6 @@ class OrderTypeCard extends StatelessWidget { /// Callback when the card is tapped. final VoidCallback onTap; - /// Creates an [OrderTypeCard]. - const OrderTypeCard({ - required this.icon, - required this.title, - required this.description, - required this.backgroundColor, - required this.borderColor, - required this.iconBackgroundColor, - required this.iconColor, - required this.textColor, - required this.descriptionColor, - required this.onTap, - super.key, - }); - @override Widget build(BuildContext context) { return GestureDetector( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart index 3bde4479..c2ce1723 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart @@ -3,6 +3,15 @@ import 'package:flutter/material.dart'; /// A card displaying an example message for a rapid order. class RapidOrderExampleCard extends StatelessWidget { + /// Creates a [RapidOrderExampleCard]. + const RapidOrderExampleCard({ + required this.example, + required this.isHighlighted, + required this.label, + required this.onTap, + super.key, + }); + /// The example text. final String example; @@ -15,15 +24,6 @@ class RapidOrderExampleCard extends StatelessWidget { /// Callback when the card is tapped. final VoidCallback onTap; - /// Creates a [RapidOrderExampleCard]. - const RapidOrderExampleCard({ - required this.example, - required this.isHighlighted, - required this.label, - required this.onTap, - super.key, - }); - @override Widget build(BuildContext context) { return GestureDetector( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart index 4d7a3848..2eec2d55 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart @@ -3,6 +3,16 @@ import 'package:flutter/material.dart'; /// A header widget for the rapid order flow with a gradient background. class RapidOrderHeader extends StatelessWidget { + /// Creates a [RapidOrderHeader]. + const RapidOrderHeader({ + required this.title, + required this.subtitle, + required this.date, + required this.time, + required this.onBack, + super.key, + }); + /// The title of the page. final String title; @@ -18,16 +28,6 @@ class RapidOrderHeader extends StatelessWidget { /// Callback when the back button is pressed. final VoidCallback onBack; - /// Creates a [RapidOrderHeader]. - const RapidOrderHeader({ - required this.title, - required this.subtitle, - required this.date, - required this.time, - required this.onBack, - super.key, - }); - @override Widget build(BuildContext context) { return Container( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart index 3ea9ad4d..e99b1bb4 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart @@ -3,6 +3,15 @@ import 'package:flutter/material.dart'; /// A view to display when a rapid order has been successfully created. class RapidOrderSuccessView extends StatelessWidget { + /// Creates a [RapidOrderSuccessView]. + const RapidOrderSuccessView({ + required this.title, + required this.message, + required this.buttonLabel, + required this.onDone, + super.key, + }); + /// The title of the success message. final String title; @@ -15,15 +24,6 @@ class RapidOrderSuccessView extends StatelessWidget { /// Callback when the completion button is tapped. final VoidCallback onDone; - /// Creates a [RapidOrderSuccessView]. - const RapidOrderSuccessView({ - required this.title, - required this.message, - required this.buttonLabel, - required this.onDone, - super.key, - }); - @override Widget build(BuildContext context) { return Scaffold( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart new file mode 100644 index 00000000..fe03182d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart @@ -0,0 +1,302 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:intl/intl.dart'; +import '../../blocs/rapid_order_bloc.dart'; +import '../../blocs/rapid_order_event.dart'; +import '../../blocs/rapid_order_state.dart'; +import 'rapid_order_example_card.dart'; +import 'rapid_order_header.dart'; +import 'rapid_order_success_view.dart'; + +/// The main content of the Rapid Order page. +class RapidOrderView extends StatelessWidget { + /// Creates a [RapidOrderView]. + const RapidOrderView({super.key}); + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderRapidEn labels = + t.client_create_order.rapid; + + return BlocBuilder( + builder: (BuildContext context, RapidOrderState state) { + if (state is RapidOrderSuccess) { + return RapidOrderSuccessView( + title: labels.success_title, + message: labels.success_message, + buttonLabel: labels.back_to_orders, + onDone: () => Modular.to.pop(), + ); + } + + return const _RapidOrderForm(); + }, + ); + } +} + +class _RapidOrderForm extends StatefulWidget { + const _RapidOrderForm(); + + @override + State<_RapidOrderForm> createState() => _RapidOrderFormState(); +} + +class _RapidOrderFormState extends State<_RapidOrderForm> { + final TextEditingController _messageController = TextEditingController(); + + @override + void dispose() { + _messageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderRapidEn labels = + t.client_create_order.rapid; + final DateTime now = DateTime.now(); + final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now); + final String timeStr = DateFormat('h:mm a').format(now); + + return BlocListener( + listener: (BuildContext context, RapidOrderState state) { + if (state is RapidOrderInitial) { + if (_messageController.text != state.message) { + _messageController.text = state.message; + _messageController.selection = TextSelection.fromPosition( + TextPosition(offset: _messageController.text.length), + ); + } + } + }, + child: Scaffold( + backgroundColor: UiColors.bgPrimary, + body: Column( + children: [ + RapidOrderHeader( + title: labels.title, + subtitle: labels.subtitle, + date: dateStr, + time: timeStr, + onBack: () => Modular.to.pop(), + ), + + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + labels.tell_us, + style: UiTypography.headline3m.textPrimary, + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.destructive, + borderRadius: UiConstants.radiusSm, + ), + child: Text( + labels.urgent_badge, + style: UiTypography.footnote2b.copyWith( + color: UiColors.white, + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space4), + + // Main Card + Container( + padding: const EdgeInsets.all(UiConstants.space6), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: BlocBuilder( + builder: (BuildContext context, RapidOrderState state) { + final RapidOrderInitial? initialState = + state is RapidOrderInitial ? state : null; + final bool isSubmitting = + state is RapidOrderSubmitting; + + return Column( + children: [ + // Icon + const _AnimatedZapIcon(), + const SizedBox(height: UiConstants.space4), + Text( + labels.need_staff, + style: UiTypography.headline2m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), + Text( + labels.type_or_speak, + textAlign: TextAlign.center, + style: UiTypography.body2r.textSecondary, + ), + const SizedBox(height: UiConstants.space6), + + // Examples + if (initialState != null) + ...initialState.examples + .asMap() + .entries + .map((MapEntry entry) { + final int index = entry.key; + final String example = entry.value; + final bool isHighlighted = index == 0; + + return Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space2), + child: RapidOrderExampleCard( + example: example, + isHighlighted: isHighlighted, + label: labels.example, + onTap: () => + BlocProvider.of( + context) + .add( + RapidOrderExampleSelected(example), + ), + ), + ); + }), + const SizedBox(height: UiConstants.space4), + + // Input + UiTextField( + controller: _messageController, + maxLines: 4, + onChanged: (String value) { + BlocProvider.of(context).add( + RapidOrderMessageChanged(value), + ); + }, + hintText: labels.hint, + ), + const SizedBox(height: UiConstants.space4), + + // Actions + _RapidOrderActions( + labels: labels, + isSubmitting: isSubmitting, + isListening: initialState?.isListening ?? false, + isMessageEmpty: initialState != null && + initialState.message.trim().isEmpty, + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _AnimatedZapIcon extends StatelessWidget { + const _AnimatedZapIcon(); + + @override + Widget build(BuildContext context) { + return Container( + width: 64, + height: 64, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.destructive, + UiColors.destructive.withValues(alpha: 0.85), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: UiConstants.radiusLg, + boxShadow: [ + BoxShadow( + color: UiColors.destructive.withValues(alpha: 0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + UiIcons.zap, + color: UiColors.white, + size: 32, + ), + ); + } +} + +class _RapidOrderActions extends StatelessWidget { + const _RapidOrderActions({ + required this.labels, + required this.isSubmitting, + required this.isListening, + required this.isMessageEmpty, + }); + final TranslationsClientCreateOrderRapidEn labels; + final bool isSubmitting; + final bool isListening; + final bool isMessageEmpty; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: UiButton.secondary( + text: isListening ? labels.listening : labels.speak, + leadingIcon: UiIcons.bell, // Placeholder for mic + onPressed: () => BlocProvider.of(context).add( + const RapidOrderVoiceToggled(), + ), + style: OutlinedButton.styleFrom( + backgroundColor: isListening + ? UiColors.destructive.withValues(alpha: 0.05) + : null, + side: isListening + ? const BorderSide(color: UiColors.destructive) + : null, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: UiButton.primary( + text: isSubmitting ? labels.sending : labels.send, + trailingIcon: UiIcons.arrowRight, + onPressed: isSubmitting || isMessageEmpty + ? null + : () => BlocProvider.of(context).add( + const RapidOrderSubmitted(), + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/pubspec.lock b/apps/mobile/packages/features/client/create_order/pubspec.lock deleted file mode 100644 index 41d3237a..00000000 --- a/apps/mobile/packages/features/client/create_order/pubspec.lock +++ /dev/null @@ -1,858 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d - url: "https://pub.dev" - source: hosted - version: "91.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 - url: "https://pub.dev" - source: hosted - version: "8.4.1" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - auto_injector: - dependency: transitive - description: - name: auto_injector - sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - bloc: - dependency: transitive - description: - name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" - url: "https://pub.dev" - source: hosted - version: "8.1.4" - bloc_test: - dependency: "direct dev" - description: - name: bloc_test - sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" - url: "https://pub.dev" - source: hosted - version: "9.1.7" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - cli_config: - dependency: transitive - description: - name: cli_config - sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.dev" - source: hosted - version: "0.2.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - core_localization: - dependency: "direct main" - description: - path: "../../../core_localization" - relative: true - source: path - version: "0.0.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.dev" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - csv: - dependency: transitive - description: - name: csv - sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c - url: "https://pub.dev" - source: hosted - version: "6.0.0" - design_system: - dependency: "direct main" - description: - path: "../../../design_system" - relative: true - source: path - version: "0.0.1" - diff_match_patch: - dependency: transitive - description: - name: diff_match_patch - sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" - url: "https://pub.dev" - source: hosted - version: "0.4.1" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" - source: hosted - version: "2.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c - url: "https://pub.dev" - source: hosted - version: "2.1.5" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_bloc: - dependency: "direct main" - description: - name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a - url: "https://pub.dev" - source: hosted - version: "8.1.6" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - flutter_modular: - dependency: "direct main" - description: - name: flutter_modular - sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - font_awesome_flutter: - dependency: transitive - description: - name: font_awesome_flutter - sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0 - url: "https://pub.dev" - source: hosted - version: "10.12.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - google_fonts: - dependency: transitive - description: - name: google_fonts - sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3" - url: "https://pub.dev" - source: hosted - version: "7.1.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" - krow_core: - dependency: "direct main" - description: - path: "../../../core" - relative: true - source: path - version: "0.0.1" - krow_data_connect: - dependency: "direct main" - description: - path: "../../../data_connect" - relative: true - source: path - version: "0.0.1" - krow_domain: - dependency: "direct main" - description: - path: "../../../domain" - relative: true - source: path - version: "0.0.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - lucide_icons: - dependency: transitive - description: - name: lucide_icons - sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4 - url: "https://pub.dev" - source: hosted - version: "0.257.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mocktail: - dependency: transitive - description: - name: mocktail - sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - modular_core: - dependency: transitive - description: - name: modular_core - sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "9922a1ad59ac5afb154cc948aa6ded01987a75003651d0a2866afc23f4da624e" - url: "https://pub.dev" - source: hosted - version: "9.2.3" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pool: - dependency: transitive - description: - name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" - source: hosted - version: "1.5.2" - provider: - dependency: transitive - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - result_dart: - dependency: transitive - description: - name: result_dart - sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" - url: "https://pub.dev" - source: hosted - version: "2.4.18" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" - source: hosted - version: "1.1.3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - slang: - dependency: transitive - description: - name: slang - sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - slang_flutter: - dependency: transitive - description: - name: slang_flutter - sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" - source: hosted - version: "0.10.13" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test: - dependency: transitive - description: - name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" - url: "https://pub.dev" - source: hosted - version: "1.26.3" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - test_core: - dependency: transitive - description: - name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" - url: "https://pub.dev" - source: hosted - version: "0.6.12" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.4" diff --git a/apps/mobile/packages/features/client/create_order/pubspec.yaml b/apps/mobile/packages/features/client/create_order/pubspec.yaml index 6ff66afe..0c1a1590 100644 --- a/apps/mobile/packages/features/client/create_order/pubspec.yaml +++ b/apps/mobile/packages/features/client/create_order/pubspec.yaml @@ -2,9 +2,10 @@ name: client_create_order description: Client create order feature version: 0.0.1 publish_to: none +resolution: workspace environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.10.0 <4.0.0" dependencies: flutter: diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 36934696..903fdd09 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -185,13 +185,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" - client_create_order: - dependency: transitive - description: - path: "packages/features/client/create_order" - relative: true - source: path - version: "0.0.1" clock: dependency: transitive description: diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 73112335..1f8f5ec3 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -14,6 +14,8 @@ workspace: - packages/features/client/home - packages/features/client/settings - packages/features/client/hubs + - packages/features/client/create_order + - packages/features/client/client_main - apps/staff - apps/client - apps/design_system_viewer