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