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