From 9e513d96afa595fdb8eed2448961bbba6e7a7caf Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Fri, 23 Jan 2026 01:55:12 -0500 Subject: [PATCH] Refactor create order UI and improve architecture Refactored the client create order feature to move UI logic for order type selection into a dedicated CreateOrderView widget, extracted order type UI metadata to a separate entity, and updated page widgets to delegate to new view components. Improved code documentation and structure for arguments, use cases, and repository interfaces. Added new localization keys and minor design system icon update. --- .../lib/src/l10n/en.i18n.json | 6 +- .../lib/src/l10n/es.i18n.json | 3 +- .../design_system/lib/src/ui_icons.dart | 3 + .../create_order/lib/client_create_order.dart | 3 +- .../lib/src/create_order_module.dart | 4 +- .../client_create_order_repository_impl.dart | 22 +- .../arguments/one_time_order_arguments.dart | 8 +- .../arguments/rapid_order_arguments.dart | 8 +- ...ent_create_order_repository_interface.dart | 18 +- .../create_one_time_order_usecase.dart | 8 +- .../usecases/create_rapid_order_usecase.dart | 7 +- .../usecases/get_order_types_usecase.dart | 7 +- .../blocs/client_create_order_bloc.dart | 1 - .../blocs/client_create_order_event.dart | 1 - .../blocs/client_create_order_state.dart | 2 +- .../blocs/one_time_order_bloc.dart | 1 - .../presentation/blocs/rapid_order_bloc.dart | 1 - .../presentation/blocs/rapid_order_event.dart | 2 - .../presentation/blocs/rapid_order_state.dart | 2 - .../presentation/pages/create_order_page.dart | 220 +------ .../pages/one_time_order_page.dart | 196 +------ .../presentation/pages/rapid_order_page.dart | 314 +--------- .../ui_entities/order_type_ui_metadata.dart | 93 +++ .../create_order/create_order_view.dart | 129 ++++ .../one_time_order_date_picker.dart | 102 ++-- .../one_time_order/one_time_order_header.dart | 73 +++ .../one_time_order_location_input.dart | 58 +- .../one_time_order_position_card.dart | 552 +++++++++++------- .../one_time_order_section_header.dart | 27 +- .../one_time_order_success_view.dart | 120 ++-- .../one_time_order/one_time_order_view.dart | 185 ++++++ .../presentation/widgets/order_type_card.dart | 30 +- .../rapid_order/rapid_order_example_card.dart | 18 +- .../rapid_order/rapid_order_header.dart | 20 +- .../rapid_order/rapid_order_success_view.dart | 18 +- .../widgets/rapid_order/rapid_order_view.dart | 302 ++++++++++ 36 files changed, 1451 insertions(+), 1113 deletions(-) create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/ui_entities/order_type_ui_metadata.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart 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..29607454 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", 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..9ae3a4c7 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,8 @@ "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" }, "recurring": { "title": "Orden Recurrente", 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..a7523316 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 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..81e133fa 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,9 @@ class ClientCreateOrderModule extends Module { // Repositories i.addLazySingleton( () => ClientCreateOrderRepositoryImpl( - orderMock: i.get()), + orderMock: i.get(), + dataConnect: ExampleConnector.instance, + ), ); // UseCases 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..c32a0ac6 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,40 @@ 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. - ClientCreateOrderRepositoryImpl({required OrderRepositoryMock orderMock}) - : _orderMock = orderMock; + /// Requires the [OrderRepositoryMock] from the shared Data Connect package. + /// TODO: Inject and use ExampleConnector when real mutations are available. + ClientCreateOrderRepositoryImpl({ + required OrderRepositoryMock orderMock, + @Deprecated('Use ExampleConnector for real mutations in the future') + Object? dataConnect, + }) : _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..0f1c47c0 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( 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..df152398 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,14 +27,11 @@ 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), - style: TextButton.styleFrom( - padding: - const EdgeInsets.symmetric(horizontal: UiConstants.space2), - ), + leadingIcon: UiIcons.add, + text: actionLabel!, + iconSize: 16, ), ], ); 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(), + ), + ), + ), + ], + ); + } +}