From 7090efb583ab852c652988b7682ef3f927551737 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 22 Jan 2026 15:52:28 -0500 Subject: [PATCH 1/3] Refactor create order feature with repository pattern Introduced domain entities, repository interface, and use case for order types in the client create order feature. Moved order type data and logic from the Bloc to a repository implementation. Updated module bindings and imports to support the new architecture, improving separation of concerns and maintainability. --- apps/mobile-client/.keep | 1 - apps/mobile-staff/.keep | 1 - .../lib/src/create_order_module.dart | 5 ++ .../client_create_order_repository_impl.dart | 62 ++++++++++++++++++ .../domain/entities/create_order_type.dart | 43 +++++++++++++ .../i_client_create_order_repository.dart | 5 ++ .../usecases/get_order_types_usecase.dart | 12 ++++ .../blocs/client_create_order_bloc.dart | 63 +++---------------- .../blocs/client_create_order_state.dart | 42 +------------ .../presentation/pages/create_order_page.dart | 1 + 10 files changed, 137 insertions(+), 98 deletions(-) delete mode 100644 apps/mobile-client/.keep delete mode 100644 apps/mobile-staff/.keep create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart diff --git a/apps/mobile-client/.keep b/apps/mobile-client/.keep deleted file mode 100644 index 8b137891..00000000 --- a/apps/mobile-client/.keep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/mobile-staff/.keep b/apps/mobile-staff/.keep deleted file mode 100644 index 8b137891..00000000 --- a/apps/mobile-staff/.keep +++ /dev/null @@ -1 +0,0 @@ - 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 826ffc4b..4ac97077 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 @@ -1,5 +1,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'data/repositories/client_create_order_repository_impl.dart'; +import 'domain/repositories/i_client_create_order_repository.dart'; +import 'domain/usecases/get_order_types_usecase.dart'; import 'presentation/blocs/client_create_order_bloc.dart'; import 'presentation/pages/create_order_page.dart'; import 'presentation/pages/one_time_order_page.dart'; @@ -10,6 +13,8 @@ import 'presentation/pages/recurring_order_page.dart'; class ClientCreateOrderModule extends Module { @override void binds(Injector i) { + i.add(ClientCreateOrderRepositoryImpl.new); + i.add(GetOrderTypesUseCase.new); i.addSingleton(ClientCreateOrderBloc.new); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart new file mode 100644 index 00000000..bd179663 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart @@ -0,0 +1,62 @@ +import 'package:design_system/design_system.dart'; +import '../../domain/entities/create_order_type.dart'; +import '../../domain/repositories/i_client_create_order_repository.dart'; + +class ClientCreateOrderRepositoryImpl implements IClientCreateOrderRepository { + @override + Future> getOrderTypes() async { + // Simulating async data fetch + await Future.delayed(const Duration(milliseconds: 100)); + + return [ + const CreateOrderType( + id: 'rapid', + icon: UiIcons.zap, + titleKey: 'client_create_order.types.rapid', + descriptionKey: 'client_create_order.types.rapid_desc', + backgroundColor: UiColors.tagError, + borderColor: UiColors.destructive, + iconBackgroundColor: UiColors.tagError, + iconColor: UiColors.destructive, + textColor: UiColors.destructive, + descriptionColor: UiColors.textError, + ), + const CreateOrderType( + id: 'one-time', + icon: UiIcons.calendar, + titleKey: 'client_create_order.types.one_time', + descriptionKey: 'client_create_order.types.one_time_desc', + backgroundColor: UiColors.tagInProgress, + borderColor: UiColors.primary, + iconBackgroundColor: UiColors.tagInProgress, + iconColor: UiColors.primary, + textColor: UiColors.primary, + descriptionColor: UiColors.primary, + ), + const CreateOrderType( + id: 'recurring', + icon: UiIcons.rotateCcw, + titleKey: 'client_create_order.types.recurring', + descriptionKey: 'client_create_order.types.recurring_desc', + backgroundColor: UiColors.tagRefunded, + borderColor: UiColors.primary, + iconBackgroundColor: UiColors.tagRefunded, + iconColor: UiColors.primary, + textColor: UiColors.primary, + descriptionColor: UiColors.textSecondary, + ), + const CreateOrderType( + id: 'permanent', + icon: UiIcons.briefcase, + titleKey: 'client_create_order.types.permanent', + descriptionKey: 'client_create_order.types.permanent_desc', + backgroundColor: UiColors.tagSuccess, + borderColor: UiColors.textSuccess, + iconBackgroundColor: UiColors.tagSuccess, + iconColor: UiColors.textSuccess, + textColor: UiColors.textSuccess, + descriptionColor: UiColors.textSuccess, + ), + ]; + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart new file mode 100644 index 00000000..2fe3d98d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/widgets.dart'; + +/// Entity representing an Order Type. +class CreateOrderType extends Equatable { + final String id; + final IconData icon; + final String titleKey; // Key for translation + final String descriptionKey; // Key for translation + final Color backgroundColor; + final Color borderColor; + final Color iconBackgroundColor; + final Color iconColor; + final Color textColor; + final Color descriptionColor; + + const CreateOrderType({ + required this.id, + required this.icon, + required this.titleKey, + required this.descriptionKey, + required this.backgroundColor, + required this.borderColor, + required this.iconBackgroundColor, + required this.iconColor, + required this.textColor, + required this.descriptionColor, + }); + + @override + List get props => [ + id, + icon, + titleKey, + descriptionKey, + backgroundColor, + borderColor, + iconBackgroundColor, + iconColor, + textColor, + descriptionColor, + ]; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart new file mode 100644 index 00000000..4464df04 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart @@ -0,0 +1,5 @@ +import '../entities/create_order_type.dart'; + +abstract interface class IClientCreateOrderRepository { + Future> getOrderTypes(); +} 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 new file mode 100644 index 00000000..88037284 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/create_order_type.dart'; +import '../repositories/i_client_create_order_repository.dart'; + +class GetOrderTypesUseCase { + final IClientCreateOrderRepository _repository; + + GetOrderTypesUseCase(this._repository); + + Future> call() { + return _repository.getOrderTypes(); + } +} 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 60f08bab..975e73fc 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 @@ -1,69 +1,22 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:design_system/design_system.dart'; +import '../../domain/usecases/get_order_types_usecase.dart'; import 'client_create_order_event.dart'; import 'client_create_order_state.dart'; class ClientCreateOrderBloc extends Bloc { - ClientCreateOrderBloc() : super(const ClientCreateOrderInitial()) { + final GetOrderTypesUseCase _getOrderTypesUseCase; + + ClientCreateOrderBloc(this._getOrderTypesUseCase) + : super(const ClientCreateOrderInitial()) { on(_onTypesRequested); } - void _onTypesRequested( + Future _onTypesRequested( ClientCreateOrderTypesRequested event, Emitter emit, - ) { - // In a real app, this might come from a repository or config - final List types = [ - const CreateOrderType( - id: 'rapid', - icon: UiIcons.zap, - titleKey: 'client_create_order.types.rapid', - descriptionKey: 'client_create_order.types.rapid_desc', - backgroundColor: UiColors.tagError, // Red-ish background - borderColor: UiColors.destructive, // Red border - iconBackgroundColor: UiColors.tagError, - iconColor: UiColors.destructive, - textColor: UiColors.destructive, - descriptionColor: UiColors.textError, - ), - const CreateOrderType( - id: 'one-time', - icon: UiIcons.calendar, - titleKey: 'client_create_order.types.one_time', - descriptionKey: 'client_create_order.types.one_time_desc', - backgroundColor: UiColors.tagInProgress, // Blue-ish - borderColor: UiColors.primary, - iconBackgroundColor: UiColors.tagInProgress, - iconColor: UiColors.primary, - textColor: UiColors.primary, - descriptionColor: UiColors.primary, - ), - const CreateOrderType( - id: 'recurring', - icon: UiIcons.rotateCcw, - titleKey: 'client_create_order.types.recurring', - descriptionKey: 'client_create_order.types.recurring_desc', - backgroundColor: UiColors.tagRefunded, // Indigo-ish (Purple sub) - borderColor: UiColors.primary, // No purple, use primary or mix - iconBackgroundColor: UiColors.tagRefunded, - iconColor: UiColors.primary, - textColor: UiColors.primary, - descriptionColor: UiColors.textSecondary, - ), - const CreateOrderType( - id: 'permanent', - icon: UiIcons.briefcase, - titleKey: 'client_create_order.types.permanent', - descriptionKey: 'client_create_order.types.permanent_desc', - backgroundColor: UiColors.tagSuccess, // Green - borderColor: UiColors.textSuccess, - iconBackgroundColor: UiColors.tagSuccess, - iconColor: UiColors.textSuccess, - textColor: UiColors.textSuccess, - descriptionColor: UiColors.textSuccess, - ), - ]; + ) async { + final types = await _getOrderTypesUseCase(); emit(ClientCreateOrderLoadSuccess(types)); } } 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 d977c4c3..f6728810 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 @@ -1,47 +1,7 @@ import 'package:equatable/equatable.dart'; +import '../../domain/entities/create_order_type.dart'; import 'package:flutter/material.dart'; -/// Represents an available order type. -class CreateOrderType extends Equatable { - final String id; - final IconData icon; - final String titleKey; // Key for translation - final String descriptionKey; // Key for translation - final Color backgroundColor; - final Color borderColor; - final Color iconBackgroundColor; - final Color iconColor; - final Color textColor; - final Color descriptionColor; - - const CreateOrderType({ - required this.id, - required this.icon, - required this.titleKey, - required this.descriptionKey, - required this.backgroundColor, - required this.borderColor, - required this.iconBackgroundColor, - required this.iconColor, - required this.textColor, - required this.descriptionColor, - }); - - @override - List get props => [ - id, - icon, - titleKey, - descriptionKey, - backgroundColor, - borderColor, - iconBackgroundColor, - iconColor, - textColor, - descriptionColor, - ]; -} - abstract class ClientCreateOrderState extends Equatable { const ClientCreateOrderState(); 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 2687f435..4be2e501 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 @@ -3,6 +3,7 @@ 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 '../../domain/entities/create_order_type.dart'; import '../blocs/client_create_order_bloc.dart'; import '../blocs/client_create_order_event.dart'; import '../blocs/client_create_order_state.dart'; From 4b3125de1ae515622fcfd06b0214b56d2da65cbe Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 22 Jan 2026 16:47:39 -0500 Subject: [PATCH 2/3] Add order entities and mocks for client order feature Introduces new domain entities for order types and one-time orders, along with their positions. Adds a mock OrderRepository to the data_connect package and wires it into the module. Updates localization files for new order flows and refactors Equatable usage for consistency. Also adds a minus icon to the design system. --- .../lib/src/l10n/en.i18n.json | 52 ++ .../lib/src/l10n/es.i18n.json | 42 ++ .../data_connect/lib/krow_data_connect.dart | 2 + .../lib/src/data_connect_module.dart | 2 + .../src/mocks/business_repository_mock.dart | 2 +- .../lib/src/mocks/event_repository_mock.dart | 8 +- .../src/mocks/financial_repository_mock.dart | 4 +- .../lib/src/mocks/order_repository_mock.dart | 44 ++ .../lib/src/mocks/rating_repository_mock.dart | 2 +- .../lib/src/mocks/skill_repository_mock.dart | 4 +- .../lib/src/mocks/staff_repository_mock.dart | 2 +- .../src/mocks/support_repository_mock.dart | 4 +- .../design_system/lib/src/ui_icons.dart | 3 + .../packages/domain/lib/krow_domain.dart | 6 + .../src/entities/business/biz_contract.dart | 20 +- .../lib/src/entities/business/business.dart | 18 +- .../entities/business/business_setting.dart | 20 +- .../domain/lib/src/entities/business/hub.dart | 20 +- .../src/entities/business/hub_department.dart | 14 +- .../lib/src/entities/events/assignment.dart | 20 +- .../domain/lib/src/entities/events/event.dart | 22 +- .../lib/src/entities/events/event_shift.dart | 16 +- .../entities/events/event_shift_position.dart | 24 +- .../lib/src/entities/events/work_session.dart | 18 +- .../lib/src/entities/financial/invoice.dart | 22 +- .../entities/financial/invoice_decline.dart | 16 +- .../src/entities/financial/invoice_item.dart | 20 +- .../src/entities/financial/staff_payment.dart | 20 +- .../entities/home/home_dashboard_data.dart | 22 +- .../src/entities/orders/one_time_order.dart | 25 + .../orders/one_time_order_position.dart | 62 ++ .../lib/src/entities/orders/order_type.dart | 25 + .../src/entities/profile/accessibility.dart | 12 +- .../src/entities/profile/bank_account.dart | 20 +- .../entities/profile/emergency_contact.dart | 14 +- .../lib/src/entities/profile/schedule.dart | 18 +- .../ratings/business_staff_preference.dart | 16 +- .../lib/src/entities/ratings/penalty_log.dart | 20 +- .../src/entities/ratings/staff_rating.dart | 20 +- .../lib/src/entities/skills/certificate.dart | 14 +- .../domain/lib/src/entities/skills/skill.dart | 16 +- .../src/entities/skills/skill_category.dart | 12 +- .../lib/src/entities/skills/skill_kit.dart | 18 +- .../lib/src/entities/skills/staff_skill.dart | 20 +- .../lib/src/entities/support/addon.dart | 16 +- .../lib/src/entities/support/media.dart | 14 +- .../domain/lib/src/entities/support/tag.dart | 12 +- .../src/entities/support/working_area.dart | 18 +- .../lib/src/entities/users/biz_member.dart | 16 +- .../lib/src/entities/users/hub_member.dart | 16 +- .../lib/src/entities/users/membership.dart | 18 +- .../domain/lib/src/entities/users/staff.dart | 26 +- .../domain/lib/src/entities/users/user.dart | 16 +- .../lib/src/create_order_module.dart | 33 +- .../client_create_order_repository_impl.dart | 62 -- .../client_create_order_repository_impl.dart | 33 + .../arguments/one_time_order_arguments.dart | 13 + .../arguments/rapid_order_arguments.dart | 12 + .../domain/entities/create_order_type.dart | 43 -- ...ent_create_order_repository_interface.dart | 16 + .../i_client_create_order_repository.dart | 5 - .../create_one_time_order_usecase.dart | 20 + .../usecases/create_rapid_order_usecase.dart | 19 + .../usecases/get_order_types_usecase.dart | 19 +- .../blocs/client_create_order_bloc.dart | 6 +- .../blocs/client_create_order_event.dart | 6 +- .../blocs/client_create_order_state.dart | 13 +- .../blocs/one_time_order_bloc.dart | 93 +++ .../blocs/one_time_order_event.dart | 50 ++ .../blocs/one_time_order_state.dart | 59 ++ .../presentation/blocs/rapid_order_bloc.dart | 89 +++ .../presentation/blocs/rapid_order_event.dart | 34 + .../presentation/blocs/rapid_order_state.dart | 52 ++ .../presentation/pages/create_order_page.dart | 123 +++- .../pages/one_time_order_page.dart | 649 +++++++++++++++++- .../pages/permanent_order_page.dart | 20 +- .../presentation/pages/rapid_order_page.dart | 565 ++++++++++++++- .../pages/recurring_order_page.dart | 20 +- .../features/client/create_order/pubspec.lock | 11 +- .../features/client/create_order/pubspec.yaml | 5 + 80 files changed, 2472 insertions(+), 531 deletions(-) create mode 100644 apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart delete mode 100644 apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart delete mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart delete mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.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 97aed906..81167fa3 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 @@ -245,6 +245,58 @@ "recurring_desc": "Ongoing Weekly / Monthly Coverage", "permanent": "Permanent", "permanent_desc": "Long-Term Staffing Placement" + }, + "rapid": { + "title": "RAPID Order", + "subtitle": "Emergency staffing in minutes", + "urgent_badge": "URGENT", + "tell_us": "Tell us what you need", + "need_staff": "Need staff urgently?", + "type_or_speak": "Type or speak what you need. I'll handle the rest", + "example": "Example: ", + "hint": "Type or speak... (e.g., \"Need 5 cooks ASAP until 5am\")", + "speak": "Speak", + "listening": "Listening...", + "send": "Send Message", + "sending": "Sending...", + "success_title": "Request Sent!", + "success_message": "We're finding available workers for you right now. You'll be notified as they accept.", + "back_to_orders": "Back to Orders" + }, + "one_time": { + "title": "One-Time Order", + "subtitle": "Single event or shift request", + "create_your_order": "Create Your Order", + "date_label": "Date", + "date_hint": "Select date", + "location_label": "Location", + "location_hint": "Enter address", + "positions_title": "Positions", + "add_position": "Add Position", + "position_number": "Position $number", + "remove": "Remove", + "select_role": "Select role", + "start_label": "Start", + "end_label": "End", + "workers_label": "Workers", + "lunch_break_label": "Lunch Break", + "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." + }, + "recurring": { + "title": "Recurring Order", + "subtitle": "Ongoing weekly/monthly coverage", + "placeholder": "Recurring Order Flow (Work in Progress)" + }, + "permanent": { + "title": "Permanent Order", + "subtitle": "Long-term staffing placement", + "placeholder": "Permanent Order Flow (Work in Progress)" } } } 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 1ec7d32d..700570ef 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 @@ -245,6 +245,48 @@ "recurring_desc": "Cobertura Continua Semanal / Mensual", "permanent": "Permanente", "permanent_desc": "Colocación de Personal a Largo Plazo" + }, + "rapid": { + "title": "Orden RÁPIDA", + "subtitle": "Personal de emergencia en minutos", + "urgent_badge": "URGENTE", + "tell_us": "Dinos qué necesitas", + "need_staff": "¿Necesitas personal urgentemente?", + "type_or_speak": "Escribe o habla lo que necesitas. Yo me encargo del resto", + "example": "Ejemplo: ", + "hint": "Escribe o habla... (ej., \"Necesito 5 cocineros YA hasta las 5am\")", + "speak": "Hablar", + "listening": "Escuchando...", + "send": "Enviar Mensaje", + "sending": "Enviando...", + "success_title": "¡Solicitud Enviada!", + "success_message": "Estamos encontrando trabajadores disponibles para ti ahora mismo. Te notificaremos cuando acepten.", + "back_to_orders": "Volver a Órdenes" + }, + "one_time": { + "title": "Orden Única Vez", + "subtitle": "Evento único o petición de turno", + "create_your_order": "Crea Tu Orden", + "date_label": "Fecha", + "date_hint": "Seleccionar fecha", + "location_label": "Ubicación", + "location_hint": "Ingresar dirección", + "positions_title": "Posiciones", + "add_position": "Añadir Posición", + "position_number": "Posición $number", + "remove": "Eliminar", + "select_role": "Seleccionar rol", + "start_label": "Inicio", + "end_label": "Fin", + "workers_label": "Trabajadores", + "lunch_break_label": "Descanso para Almuerzo", + "different_location": "Usar ubicación diferente para esta posición", + "different_location_title": "Ubicación Diferente", + "different_location_hint": "Ingresar dirección diferente", + "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." } } } diff --git a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart index c7184fdb..445db229 100644 --- a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart +++ b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart @@ -5,6 +5,7 @@ /// /// TODO: These mocks currently do not implement any specific interfaces. /// They will implement interfaces defined in feature packages once those are created. +library; export 'src/mocks/auth_repository_mock.dart'; export 'src/mocks/staff_repository_mock.dart'; @@ -15,6 +16,7 @@ export 'src/mocks/rating_repository_mock.dart'; export 'src/mocks/support_repository_mock.dart'; export 'src/mocks/home_repository_mock.dart'; export 'src/mocks/business_repository_mock.dart'; +export 'src/mocks/order_repository_mock.dart'; export 'src/data_connect_module.dart'; // Export the generated Data Connect SDK diff --git a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart index 553eac80..5d1036f1 100644 --- a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart +++ b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart @@ -2,6 +2,7 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'mocks/auth_repository_mock.dart'; import 'mocks/business_repository_mock.dart'; import 'mocks/home_repository_mock.dart'; +import 'mocks/order_repository_mock.dart'; /// A module that provides Data Connect dependencies, including mocks. class DataConnectModule extends Module { @@ -11,5 +12,6 @@ class DataConnectModule extends Module { i.addLazySingleton(AuthRepositoryMock.new); i.addLazySingleton(HomeRepositoryMock.new); i.addLazySingleton(BusinessRepositoryMock.new); + i.addLazySingleton(OrderRepositoryMock.new); } } diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart index 3895c0b6..6ed624ef 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart @@ -15,7 +15,7 @@ class BusinessRepositoryMock { Future> getHubs(String businessId) async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ const Hub( id: 'hub_1', businessId: 'biz_1', diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart index 44159611..0cdd03c2 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart @@ -19,7 +19,7 @@ class EventRepositoryMock { Future> getEventShifts(String eventId) async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ const EventShift( id: 'shift_1', eventId: 'event_1', @@ -31,7 +31,7 @@ class EventRepositoryMock { Future> getStaffAssignments(String staffId) async { await Future.delayed(const Duration(milliseconds: 500)); - return [ + return [ const Assignment( id: 'assign_1', positionId: 'pos_1', @@ -43,10 +43,10 @@ class EventRepositoryMock { Future> getUpcomingEvents() async { await Future.delayed(const Duration(milliseconds: 800)); - return [_mockEvent]; + return [_mockEvent]; } - static final _mockEvent = Event( + static final Event _mockEvent = Event( id: 'event_1', businessId: 'biz_1', hubId: 'hub_1', diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart index 050cf7e5..0711463a 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; class FinancialRepositoryMock { Future> getInvoices(String businessId) async { await Future.delayed(const Duration(milliseconds: 500)); - return [ + return [ const Invoice( id: 'inv_1', eventId: 'event_1', @@ -19,7 +19,7 @@ class FinancialRepositoryMock { Future> getStaffPayments(String staffId) async { await Future.delayed(const Duration(milliseconds: 500)); - return [ + return [ StaffPayment( id: 'pay_1', staffId: staffId, diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart new file mode 100644 index 00000000..8e7979ea --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart @@ -0,0 +1,44 @@ +import 'package:krow_domain/krow_domain.dart'; + +/// Mock implementation of order-related data operations. +/// +/// This class simulates backend responses for order types and order creation. +/// It is used by the feature-level repository implementations. +class OrderRepositoryMock { + /// Returns a list of available [OrderType]s. + Future> getOrderTypes() async { + await Future.delayed(const Duration(milliseconds: 500)); + return const [ + OrderType( + id: 'rapid', + titleKey: 'client_create_order.types.rapid', + descriptionKey: 'client_create_order.types.rapid_desc', + ), + OrderType( + id: 'one-time', + titleKey: 'client_create_order.types.one_time', + descriptionKey: 'client_create_order.types.one_time_desc', + ), + OrderType( + id: 'recurring', + titleKey: 'client_create_order.types.recurring', + descriptionKey: 'client_create_order.types.recurring_desc', + ), + OrderType( + id: 'permanent', + titleKey: 'client_create_order.types.permanent', + descriptionKey: 'client_create_order.types.permanent_desc', + ), + ]; + } + + /// Simulates creating a one-time order. + Future createOneTimeOrder(OneTimeOrder order) async { + await Future.delayed(const Duration(milliseconds: 800)); + } + + /// Simulates creating a rapid order. + Future createRapidOrder(String description) async { + await Future.delayed(const Duration(seconds: 1)); + } +} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart index eedb0efb..f679fa11 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; class RatingRepositoryMock { Future> getStaffRatings(String staffId) async { await Future.delayed(const Duration(milliseconds: 400)); - return [ + return [ const StaffRating( id: 'rate_1', staffId: 'staff_1', diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart index a808733c..f60187da 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart @@ -8,7 +8,7 @@ class SkillRepositoryMock { Future> getAllSkills() async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ const Skill( id: 'skill_1', categoryId: 'cat_1', @@ -26,7 +26,7 @@ class SkillRepositoryMock { Future> getStaffSkills(String staffId) async { await Future.delayed(const Duration(milliseconds: 400)); - return [ + return [ const StaffSkill( id: 'staff_skill_1', staffId: 'staff_1', diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart index b40479ee..c032464f 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart @@ -9,7 +9,7 @@ class StaffRepositoryMock { Future> getMemberships(String userId) async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ Membership( id: 'mem_1', userId: userId, diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart index 346eb8d1..0722052e 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; class SupportRepositoryMock { Future> getTags() async { await Future.delayed(const Duration(milliseconds: 200)); - return [ + return [ const Tag(id: 'tag_1', label: 'Urgent'), const Tag(id: 'tag_2', label: 'VIP Event'), ]; @@ -12,7 +12,7 @@ class SupportRepositoryMock { Future> getWorkingAreas() async { await Future.delayed(const Duration(milliseconds: 200)); - return [ + return [ const WorkingArea( id: 'area_1', name: 'Central London', 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 99e8b1f9..60b6fb02 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_icons.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_icons.dart @@ -48,6 +48,9 @@ class UiIcons { /// Plus/Add icon static const IconData add = _IconLib.plus; + /// Minus icon + static const IconData minus = _IconLib.minus; + /// Edit icon static const IconData edit = _IconLib.edit2; diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 63e416fc..07c99633 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -4,6 +4,7 @@ /// It is pure Dart and has no dependencies on Flutter or Firebase. /// /// Note: Repository Interfaces are now located in their respective Feature packages. +library; // Users & Membership export 'src/entities/users/user.dart'; @@ -26,6 +27,11 @@ export 'src/entities/events/event_shift_position.dart'; export 'src/entities/events/assignment.dart'; export 'src/entities/events/work_session.dart'; +// Orders & Requests +export 'src/entities/orders/order_type.dart'; +export 'src/entities/orders/one_time_order.dart'; +export 'src/entities/orders/one_time_order_position.dart'; + // Skills & Certs export 'src/entities/skills/skill.dart'; export 'src/entities/skills/skill_category.dart'; diff --git a/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart b/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart index 81ebf648..196c9eb8 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart @@ -4,6 +4,15 @@ import 'package:equatable/equatable.dart'; /// /// Can be between a business and the platform, or a business and staff. class BizContract extends Equatable { + + const BizContract({ + required this.id, + required this.businessId, + required this.name, + required this.startDate, + this.endDate, + required this.contentUrl, + }); /// Unique identifier. final String id; @@ -22,15 +31,6 @@ class BizContract extends Equatable { /// URL to the document content (PDF/HTML). final String contentUrl; - const BizContract({ - required this.id, - required this.businessId, - required this.name, - required this.startDate, - this.endDate, - required this.contentUrl, - }); - @override - List get props => [id, businessId, name, startDate, endDate, contentUrl]; + List get props => [id, businessId, name, startDate, endDate, contentUrl]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/business/business.dart b/apps/mobile/packages/domain/lib/src/entities/business/business.dart index a719d748..c03e75c9 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/business.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/business.dart @@ -19,6 +19,14 @@ enum BusinessStatus { /// /// This is the top-level organizational entity in the system. class Business extends Equatable { + + const Business({ + required this.id, + required this.name, + required this.registrationNumber, + required this.status, + this.avatar, + }); /// Unique identifier for the business. final String id; @@ -34,14 +42,6 @@ class Business extends Equatable { /// URL to the business logo. final String? avatar; - const Business({ - required this.id, - required this.name, - required this.registrationNumber, - required this.status, - this.avatar, - }); - @override - List get props => [id, name, registrationNumber, status, avatar]; + List get props => [id, name, registrationNumber, status, avatar]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart b/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart index b9f62bd0..328cb39c 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart @@ -2,6 +2,15 @@ import 'package:equatable/equatable.dart'; /// Represents payroll and operational configuration for a [Business]. class BusinessSetting extends Equatable { + + const BusinessSetting({ + required this.id, + required this.businessId, + required this.prefix, + required this.overtimeEnabled, + this.clockInRequirement, + this.clockOutRequirement, + }); /// Unique identifier for the settings record. final String id; @@ -20,17 +29,8 @@ class BusinessSetting extends Equatable { /// Requirement method for clocking out. final String? clockOutRequirement; - const BusinessSetting({ - required this.id, - required this.businessId, - required this.prefix, - required this.overtimeEnabled, - this.clockInRequirement, - this.clockOutRequirement, - }); - @override - List get props => [ + List get props => [ id, businessId, prefix, diff --git a/apps/mobile/packages/domain/lib/src/entities/business/hub.dart b/apps/mobile/packages/domain/lib/src/entities/business/hub.dart index 400d3bfe..4070a28a 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/hub.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/hub.dart @@ -14,6 +14,15 @@ enum HubStatus { /// Represents a branch location or operational unit within a [Business]. class Hub extends Equatable { + + const Hub({ + required this.id, + required this.businessId, + required this.name, + required this.address, + this.nfcTagId, + required this.status, + }); /// Unique identifier. final String id; @@ -32,15 +41,6 @@ class Hub extends Equatable { /// Operational status. final HubStatus status; - const Hub({ - required this.id, - required this.businessId, - required this.name, - required this.address, - this.nfcTagId, - required this.status, - }); - @override - List get props => [id, businessId, name, address, nfcTagId, status]; + List get props => [id, businessId, name, address, nfcTagId, status]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart b/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart index 0e8f523e..3c6891bc 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Used for more granular organization of staff and events (e.g. "Kitchen", "Service"). class HubDepartment extends Equatable { + + const HubDepartment({ + required this.id, + required this.hubId, + required this.name, + }); /// Unique identifier. final String id; @@ -13,12 +19,6 @@ class HubDepartment extends Equatable { /// Name of the department. final String name; - const HubDepartment({ - required this.id, - required this.hubId, - required this.name, - }); - @override - List get props => [id, hubId, name]; + List get props => [id, hubId, name]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart b/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart index 26795977..197281a5 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart @@ -26,6 +26,15 @@ enum AssignmentStatus { /// Represents the link between a [Staff] member and an [EventShiftPosition]. class Assignment extends Equatable { + + const Assignment({ + required this.id, + required this.positionId, + required this.staffId, + required this.status, + this.clockIn, + this.clockOut, + }); /// Unique identifier. final String id; @@ -44,15 +53,6 @@ class Assignment extends Equatable { /// Actual timestamp when staff clocked out. final DateTime? clockOut; - const Assignment({ - required this.id, - required this.positionId, - required this.staffId, - required this.status, - this.clockIn, - this.clockOut, - }); - @override - List get props => [id, positionId, staffId, status, clockIn, clockOut]; + List get props => [id, positionId, staffId, status, clockIn, clockOut]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event.dart b/apps/mobile/packages/domain/lib/src/entities/events/event.dart index 717fb24a..d7def36f 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/event.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/event.dart @@ -34,6 +34,16 @@ enum EventStatus { /// /// This is the central entity for scheduling work. An Event contains [EventShift]s. class Event extends Equatable { + + const Event({ + required this.id, + required this.businessId, + required this.hubId, + required this.name, + required this.date, + required this.status, + required this.contractType, + }); /// Unique identifier. final String id; @@ -55,16 +65,6 @@ class Event extends Equatable { /// Type of employment contract (e.g., 'freelance', 'permanent'). final String contractType; - const Event({ - required this.id, - required this.businessId, - required this.hubId, - required this.name, - required this.date, - required this.status, - required this.contractType, - }); - @override - List get props => [id, businessId, hubId, name, date, status, contractType]; + List get props => [id, businessId, hubId, name, date, status, contractType]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart b/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart index d5218e19..32a025e3 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart @@ -4,6 +4,13 @@ import 'package:equatable/equatable.dart'; /// /// An Event can have multiple shifts (e.g. "Morning Shift", "Evening Shift"). class EventShift extends Equatable { + + const EventShift({ + required this.id, + required this.eventId, + required this.name, + required this.address, + }); /// Unique identifier. final String id; @@ -16,13 +23,6 @@ class EventShift extends Equatable { /// Specific address for this shift (if different from Hub). final String address; - const EventShift({ - required this.id, - required this.eventId, - required this.name, - required this.address, - }); - @override - List get props => [id, eventId, name, address]; + List get props => [id, eventId, name, address]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart b/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart index abceb7b9..8eb226f0 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart @@ -4,6 +4,17 @@ import 'package:equatable/equatable.dart'; /// /// Defines the requirement for a specific [Skill], the quantity needed, and the pay. class EventShiftPosition extends Equatable { + + const EventShiftPosition({ + required this.id, + required this.shiftId, + required this.skillId, + required this.count, + required this.rate, + required this.startTime, + required this.endTime, + required this.breakDurationMinutes, + }); /// Unique identifier. final String id; @@ -28,19 +39,8 @@ class EventShiftPosition extends Equatable { /// Deducted break duration in minutes. final int breakDurationMinutes; - const EventShiftPosition({ - required this.id, - required this.shiftId, - required this.skillId, - required this.count, - required this.rate, - required this.startTime, - required this.endTime, - required this.breakDurationMinutes, - }); - @override - List get props => [ + List get props => [ id, shiftId, skillId, diff --git a/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart b/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart index 319606bd..ef06a323 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Derived from [Assignment] clock-in/out times, used for payroll. class WorkSession extends Equatable { + + const WorkSession({ + required this.id, + required this.assignmentId, + required this.startTime, + this.endTime, + required this.breakDurationMinutes, + }); /// Unique identifier. final String id; @@ -19,14 +27,6 @@ class WorkSession extends Equatable { /// Verified break duration. final int breakDurationMinutes; - const WorkSession({ - required this.id, - required this.assignmentId, - required this.startTime, - this.endTime, - required this.breakDurationMinutes, - }); - @override - List get props => [id, assignmentId, startTime, endTime, breakDurationMinutes]; + List get props => [id, assignmentId, startTime, endTime, breakDurationMinutes]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart index 7775d775..2dc06f9c 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart @@ -26,6 +26,16 @@ enum InvoiceStatus { /// Represents a bill sent to a [Business] for services rendered. class Invoice extends Equatable { + + const Invoice({ + required this.id, + required this.eventId, + required this.businessId, + required this.status, + required this.totalAmount, + required this.workAmount, + required this.addonsAmount, + }); /// Unique identifier. final String id; @@ -47,18 +57,8 @@ class Invoice extends Equatable { /// Total amount for addons/extras. final double addonsAmount; - const Invoice({ - required this.id, - required this.eventId, - required this.businessId, - required this.status, - required this.totalAmount, - required this.workAmount, - required this.addonsAmount, - }); - @override - List get props => [ + List get props => [ id, eventId, businessId, diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart index 17d7afc4..1d0a8035 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart @@ -2,6 +2,13 @@ import 'package:equatable/equatable.dart'; /// Represents a reason or log for a declined [Invoice]. class InvoiceDecline extends Equatable { + + const InvoiceDecline({ + required this.id, + required this.invoiceId, + required this.reason, + required this.declinedAt, + }); /// Unique identifier. final String id; @@ -14,13 +21,6 @@ class InvoiceDecline extends Equatable { /// When the decline happened. final DateTime declinedAt; - const InvoiceDecline({ - required this.id, - required this.invoiceId, - required this.reason, - required this.declinedAt, - }); - @override - List get props => [id, invoiceId, reason, declinedAt]; + List get props => [id, invoiceId, reason, declinedAt]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart index b290d7b1..e661334a 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart @@ -4,6 +4,15 @@ import 'package:equatable/equatable.dart'; /// /// Corresponds to the work done by one [Staff] member. class InvoiceItem extends Equatable { + + const InvoiceItem({ + required this.id, + required this.invoiceId, + required this.staffId, + required this.workHours, + required this.rate, + required this.amount, + }); /// Unique identifier. final String id; @@ -22,15 +31,6 @@ class InvoiceItem extends Equatable { /// Total line item amount (workHours * rate). final double amount; - const InvoiceItem({ - required this.id, - required this.invoiceId, - required this.staffId, - required this.workHours, - required this.rate, - required this.amount, - }); - @override - List get props => [id, invoiceId, staffId, workHours, rate, amount]; + List get props => [id, invoiceId, staffId, workHours, rate, amount]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart index ed8cd75c..bd890a77 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart @@ -17,6 +17,15 @@ enum PaymentStatus { /// Represents a payout to a [Staff] member for a completed [Assignment]. class StaffPayment extends Equatable { + + const StaffPayment({ + required this.id, + required this.staffId, + required this.assignmentId, + required this.amount, + required this.status, + this.paidAt, + }); /// Unique identifier. final String id; @@ -35,15 +44,6 @@ class StaffPayment extends Equatable { /// When the payment was successfully processed. final DateTime? paidAt; - const StaffPayment({ - required this.id, - required this.staffId, - required this.assignmentId, - required this.amount, - required this.status, - this.paidAt, - }); - @override - List get props => [id, staffId, assignmentId, amount, status, paidAt]; + List get props => [id, staffId, assignmentId, amount, status, paidAt]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart b/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart index 124f7d65..681e7a22 100644 --- a/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart +++ b/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart @@ -5,6 +5,16 @@ import 'package:equatable/equatable.dart'; /// This entity provides aggregated metrics such as spending and shift counts /// for both the current week and the upcoming 7 days. class HomeDashboardData extends Equatable { + + /// Creates a [HomeDashboardData] instance. + const HomeDashboardData({ + required this.weeklySpending, + required this.next7DaysSpending, + required this.weeklyShifts, + required this.next7DaysScheduled, + required this.totalNeeded, + required this.totalFilled, + }); /// Total spending for the current week. final double weeklySpending; @@ -23,18 +33,8 @@ class HomeDashboardData extends Equatable { /// Total workers filled for today's shifts. final int totalFilled; - /// Creates a [HomeDashboardData] instance. - const HomeDashboardData({ - required this.weeklySpending, - required this.next7DaysSpending, - required this.weeklyShifts, - required this.next7DaysScheduled, - required this.totalNeeded, - required this.totalFilled, - }); - @override - List get props => [ + List get props => [ weeklySpending, next7DaysSpending, weeklyShifts, diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart new file mode 100644 index 00000000..7fb15c9a --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; +import 'one_time_order_position.dart'; + +/// Represents a customer's request for a single event or shift. +/// +/// Encapsulates the date, primary location, and a list of specific [OneTimeOrderPosition] requirements. +class OneTimeOrder extends Equatable { + + const OneTimeOrder({ + required this.date, + required this.location, + required this.positions, + }); + /// The specific date for the shift or event. + final DateTime date; + + /// The primary location where the work will take place. + final String location; + + /// The list of positions and headcounts required for this order. + final List positions; + + @override + List get props => [date, location, positions]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart new file mode 100644 index 00000000..b8a09b7e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart @@ -0,0 +1,62 @@ +import 'package:equatable/equatable.dart'; + +/// Represents a specific position requirement within a [OneTimeOrder]. +/// +/// Defines the role, headcount, and scheduling details for a single staffing requirement. +class OneTimeOrderPosition extends Equatable { + + const OneTimeOrderPosition({ + required this.role, + required this.count, + required this.startTime, + required this.endTime, + this.lunchBreak = 30, + this.location, + }); + /// The job role or title required. + final String role; + + /// The number of workers required for this position. + final int count; + + /// The scheduled start time (e.g., "09:00 AM"). + final String startTime; + + /// The scheduled end time (e.g., "05:00 PM"). + final String endTime; + + /// The duration of the lunch break in minutes. Defaults to 30. + final int lunchBreak; + + /// Optional specific location for this position, if different from the order's main location. + final String? location; + + @override + List get props => [ + role, + count, + startTime, + endTime, + lunchBreak, + location, + ]; + + /// Creates a copy of this position with the given fields replaced. + OneTimeOrderPosition copyWith({ + String? role, + int? count, + String? startTime, + String? endTime, + int? lunchBreak, + String? location, + }) { + return OneTimeOrderPosition( + role: role ?? this.role, + count: count ?? this.count, + startTime: startTime ?? this.startTime, + endTime: endTime ?? this.endTime, + lunchBreak: lunchBreak ?? this.lunchBreak, + location: location ?? this.location, + ); + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart b/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart new file mode 100644 index 00000000..e1448be7 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +/// Represents a type of order that can be created (e.g., Rapid, One-Time). +/// +/// This entity defines the identity and display metadata (keys) for the order type. +/// UI-specific properties like colors and icons are handled by the presentation layer. +class OrderType extends Equatable { + + const OrderType({ + required this.id, + required this.titleKey, + required this.descriptionKey, + }); + /// Unique identifier for the order type. + final String id; + + /// Translation key for the title. + final String titleKey; + + /// Translation key for the description. + final String descriptionKey; + + @override + List get props => [id, titleKey, descriptionKey]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart b/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart index 22169d82..263b5550 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart @@ -4,17 +4,17 @@ import 'package:equatable/equatable.dart'; /// /// Can apply to Staff (needs) or Events (provision). class Accessibility extends Equatable { + + const Accessibility({ + required this.id, + required this.name, + }); /// Unique identifier. final String id; /// Description (e.g. "Wheelchair Access"). final String name; - const Accessibility({ - required this.id, - required this.name, - }); - @override - List get props => [id, name]; + List get props => [id, name]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart index 04b74224..91ff1f5b 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart @@ -2,6 +2,15 @@ import 'package:equatable/equatable.dart'; /// Represents bank account details for payroll. class BankAccount extends Equatable { + + const BankAccount({ + required this.id, + required this.userId, + required this.bankName, + required this.accountNumber, + required this.accountName, + this.sortCode, + }); /// Unique identifier. final String id; @@ -20,15 +29,6 @@ class BankAccount extends Equatable { /// Sort code (if applicable). final String? sortCode; - const BankAccount({ - required this.id, - required this.userId, - required this.bankName, - required this.accountNumber, - required this.accountName, - this.sortCode, - }); - @override - List get props => [id, userId, bankName, accountNumber, accountName, sortCode]; + List get props => [id, userId, bankName, accountNumber, accountName, sortCode]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart b/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart index 99ffe704..d9e8fcd2 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Critical for staff safety during shifts. class EmergencyContact extends Equatable { + + const EmergencyContact({ + required this.name, + required this.relationship, + required this.phone, + }); /// Full name of the contact. final String name; @@ -13,12 +19,6 @@ class EmergencyContact extends Equatable { /// Phone number. final String phone; - const EmergencyContact({ - required this.name, - required this.relationship, - required this.phone, - }); - @override - List get props => [name, relationship, phone]; + List get props => [name, relationship, phone]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart b/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart index 40276a20..5aeb8131 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Defines recurring availability (e.g., "Mondays 9-5"). class Schedule extends Equatable { + + const Schedule({ + required this.id, + required this.staffId, + required this.dayOfWeek, + required this.startTime, + required this.endTime, + }); /// Unique identifier. final String id; @@ -19,14 +27,6 @@ class Schedule extends Equatable { /// End time of availability. final DateTime endTime; - const Schedule({ - required this.id, - required this.staffId, - required this.dayOfWeek, - required this.startTime, - required this.endTime, - }); - @override - List get props => [id, staffId, dayOfWeek, startTime, endTime]; + List get props => [id, staffId, dayOfWeek, startTime, endTime]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart index 1c4ea3af..1f56eecb 100644 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart +++ b/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart @@ -11,6 +11,13 @@ enum PreferenceType { /// Represents a business's specific preference for a staff member. class BusinessStaffPreference extends Equatable { + + const BusinessStaffPreference({ + required this.id, + required this.businessId, + required this.staffId, + required this.type, + }); /// Unique identifier. final String id; @@ -23,13 +30,6 @@ class BusinessStaffPreference extends Equatable { /// Whether they are a favorite or blocked. final PreferenceType type; - const BusinessStaffPreference({ - required this.id, - required this.businessId, - required this.staffId, - required this.type, - }); - @override - List get props => [id, businessId, staffId, type]; + List get props => [id, businessId, staffId, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart index 317b6dd6..d42e46f0 100644 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart +++ b/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart @@ -4,6 +4,15 @@ import 'package:equatable/equatable.dart'; /// /// Penalties are issued for no-shows, cancellations, or poor conduct. class PenaltyLog extends Equatable { + + const PenaltyLog({ + required this.id, + required this.staffId, + required this.assignmentId, + required this.reason, + required this.points, + required this.issuedAt, + }); /// Unique identifier. final String id; @@ -22,15 +31,6 @@ class PenaltyLog extends Equatable { /// When the penalty was issued. final DateTime issuedAt; - const PenaltyLog({ - required this.id, - required this.staffId, - required this.assignmentId, - required this.reason, - required this.points, - required this.issuedAt, - }); - @override - List get props => [id, staffId, assignmentId, reason, points, issuedAt]; + List get props => [id, staffId, assignmentId, reason, points, issuedAt]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart index 635dcc0b..b51a44ae 100644 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart +++ b/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart @@ -2,6 +2,15 @@ import 'package:equatable/equatable.dart'; /// Represents a rating given to a staff member by a client. class StaffRating extends Equatable { + + const StaffRating({ + required this.id, + required this.staffId, + required this.eventId, + required this.businessId, + required this.rating, + this.comment, + }); /// Unique identifier. final String id; @@ -20,15 +29,6 @@ class StaffRating extends Equatable { /// Optional feedback text. final String? comment; - const StaffRating({ - required this.id, - required this.staffId, - required this.eventId, - required this.businessId, - required this.rating, - this.comment, - }); - @override - List get props => [id, staffId, eventId, businessId, rating, comment]; + List get props => [id, staffId, eventId, businessId, rating, comment]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart b/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart index 362832c0..fd6065f8 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Examples: "Food Hygiene Level 2", "SIA Badge". class Certificate extends Equatable { + + const Certificate({ + required this.id, + required this.name, + required this.isRequired, + }); /// Unique identifier. final String id; @@ -13,12 +19,6 @@ class Certificate extends Equatable { /// Whether this certificate is mandatory for platform access or specific roles. final bool isRequired; - const Certificate({ - required this.id, - required this.name, - required this.isRequired, - }); - @override - List get props => [id, name, isRequired]; + List get props => [id, name, isRequired]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart index a5d11320..f61b68e7 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart @@ -5,6 +5,13 @@ import 'package:equatable/equatable.dart'; /// Examples: "Waiter", "Security Guard", "Bartender". /// Linked to a [SkillCategory]. class Skill extends Equatable { + + const Skill({ + required this.id, + required this.categoryId, + required this.name, + required this.basePrice, + }); /// Unique identifier. final String id; @@ -17,13 +24,6 @@ class Skill extends Equatable { /// Default hourly rate suggested for this skill. final double basePrice; - const Skill({ - required this.id, - required this.categoryId, - required this.name, - required this.basePrice, - }); - @override - List get props => [id, categoryId, name, basePrice]; + List get props => [id, categoryId, name, basePrice]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart index 063dedb8..091fce05 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart @@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart'; /// Represents a broad category of skills (e.g. "Hospitality", "Logistics"). class SkillCategory extends Equatable { + + const SkillCategory({ + required this.id, + required this.name, + }); /// Unique identifier. final String id; /// Display name. final String name; - const SkillCategory({ - required this.id, - required this.name, - }); - @override - List get props => [id, name]; + List get props => [id, name]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart index a92b8bd2..eca88467 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Examples: "Black Shirt" (Uniform), "Safety Boots" (Equipment). class SkillKit extends Equatable { + + const SkillKit({ + required this.id, + required this.skillId, + required this.name, + required this.isRequired, + required this.type, + }); /// Unique identifier. final String id; @@ -19,14 +27,6 @@ class SkillKit extends Equatable { /// Type of kit ('uniform' or 'equipment'). final String type; - const SkillKit({ - required this.id, - required this.skillId, - required this.name, - required this.isRequired, - required this.type, - }); - @override - List get props => [id, skillId, name, isRequired, type]; + List get props => [id, skillId, name, isRequired, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart b/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart index da54471f..b868c9d7 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart @@ -26,6 +26,15 @@ enum StaffSkillStatus { /// Represents a staff member's qualification in a specific [Skill]. class StaffSkill extends Equatable { + + const StaffSkill({ + required this.id, + required this.staffId, + required this.skillId, + required this.level, + required this.experienceYears, + required this.status, + }); /// Unique identifier. final String id; @@ -44,15 +53,6 @@ class StaffSkill extends Equatable { /// Verification status. final StaffSkillStatus status; - const StaffSkill({ - required this.id, - required this.staffId, - required this.skillId, - required this.level, - required this.experienceYears, - required this.status, - }); - @override - List get props => [id, staffId, skillId, level, experienceYears, status]; + List get props => [id, staffId, skillId, level, experienceYears, status]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/addon.dart b/apps/mobile/packages/domain/lib/src/entities/support/addon.dart index 9a78353f..fd85edba 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/addon.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/addon.dart @@ -2,6 +2,13 @@ import 'package:equatable/equatable.dart'; /// Represents a financial addon/bonus/deduction applied to an Invoice or Payment. class Addon extends Equatable { + + const Addon({ + required this.id, + required this.name, + required this.amount, + required this.type, + }); /// Unique identifier. final String id; @@ -14,13 +21,6 @@ class Addon extends Equatable { /// Type ('credit' or 'debit'). final String type; - const Addon({ - required this.id, - required this.name, - required this.amount, - required this.type, - }); - @override - List get props => [id, name, amount, type]; + List get props => [id, name, amount, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/media.dart b/apps/mobile/packages/domain/lib/src/entities/support/media.dart index 8b298b61..329cdca6 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/media.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/media.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Used for avatars, certificates, or event photos. class Media extends Equatable { + + const Media({ + required this.id, + required this.url, + required this.type, + }); /// Unique identifier. final String id; @@ -13,12 +19,6 @@ class Media extends Equatable { /// MIME type or general type (image, pdf). final String type; - const Media({ - required this.id, - required this.url, - required this.type, - }); - @override - List get props => [id, url, type]; + List get props => [id, url, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/tag.dart b/apps/mobile/packages/domain/lib/src/entities/support/tag.dart index 62deacaa..44d4db9d 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/tag.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/tag.dart @@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart'; /// Represents a descriptive tag used for categorizing events or staff. class Tag extends Equatable { + + const Tag({ + required this.id, + required this.label, + }); /// Unique identifier. final String id; /// Text label. final String label; - const Tag({ - required this.id, - required this.label, - }); - @override - List get props => [id, label]; + List get props => [id, label]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart b/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart index cc044b4c..aa5d8d56 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart @@ -2,6 +2,14 @@ import 'package:equatable/equatable.dart'; /// Represents a geographical area where a [Staff] member is willing to work. class WorkingArea extends Equatable { + + const WorkingArea({ + required this.id, + required this.name, + required this.centerLat, + required this.centerLng, + required this.radiusKm, + }); /// Unique identifier. final String id; @@ -17,14 +25,6 @@ class WorkingArea extends Equatable { /// Radius in Kilometers. final double radiusKm; - const WorkingArea({ - required this.id, - required this.name, - required this.centerLat, - required this.centerLng, - required this.radiusKm, - }); - @override - List get props => [id, name, centerLat, centerLng, radiusKm]; + List get props => [id, name, centerLat, centerLng, radiusKm]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart b/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart index fc7b8099..2f3bcf34 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart @@ -4,6 +4,13 @@ import 'package:equatable/equatable.dart'; /// /// Grants a user access to business-level operations. class BizMember extends Equatable { + + const BizMember({ + required this.id, + required this.businessId, + required this.userId, + required this.role, + }); /// Unique identifier for this membership. final String id; @@ -16,13 +23,6 @@ class BizMember extends Equatable { /// The role within the business. final String role; - const BizMember({ - required this.id, - required this.businessId, - required this.userId, - required this.role, - }); - @override - List get props => [id, businessId, userId, role]; + List get props => [id, businessId, userId, role]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart b/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart index 0ef66a18..a6bd7a7f 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart @@ -4,6 +4,13 @@ import 'package:equatable/equatable.dart'; /// /// Grants a user access to specific [Hub] operations, distinct from [BizMember]. class HubMember extends Equatable { + + const HubMember({ + required this.id, + required this.hubId, + required this.userId, + required this.role, + }); /// Unique identifier for this membership. final String id; @@ -16,13 +23,6 @@ class HubMember extends Equatable { /// The role within the hub. final String role; - const HubMember({ - required this.id, - required this.hubId, - required this.userId, - required this.role, - }); - @override - List get props => [id, hubId, userId, role]; + List get props => [id, hubId, userId, role]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/membership.dart b/apps/mobile/packages/domain/lib/src/entities/users/membership.dart index be5a0587..c09ea2ae 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/membership.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/membership.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Allows a [User] to be a member of either a [Business] or a [Hub]. class Membership extends Equatable { + + const Membership({ + required this.id, + required this.userId, + required this.memberableId, + required this.memberableType, + required this.role, + }); /// Unique identifier for the membership record. final String id; @@ -19,14 +27,6 @@ class Membership extends Equatable { /// The role within that organization (e.g., 'manager', 'viewer'). final String role; - const Membership({ - required this.id, - required this.userId, - required this.memberableId, - required this.memberableType, - required this.role, - }); - @override - List get props => [id, userId, memberableId, memberableType, role]; + List get props => [id, userId, memberableId, memberableType, role]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/staff.dart b/apps/mobile/packages/domain/lib/src/entities/users/staff.dart index 29f417cb..f3bc2bf0 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/staff.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/staff.dart @@ -29,6 +29,18 @@ enum StaffStatus { /// Contains all personal and professional details of a staff member. /// Linked to a [User] via [authProviderId]. class Staff extends Equatable { + + const Staff({ + required this.id, + required this.authProviderId, + required this.name, + required this.email, + this.phone, + required this.status, + this.address, + this.avatar, + this.livePhoto, + }); /// Unique identifier for the staff profile. final String id; @@ -56,20 +68,8 @@ class Staff extends Equatable { /// URL to a verified live photo for identity verification. final String? livePhoto; - const Staff({ - required this.id, - required this.authProviderId, - required this.name, - required this.email, - this.phone, - required this.status, - this.address, - this.avatar, - this.livePhoto, - }); - @override - List get props => [ + List get props => [ id, authProviderId, name, diff --git a/apps/mobile/packages/domain/lib/src/entities/users/user.dart b/apps/mobile/packages/domain/lib/src/entities/users/user.dart index bc1b3e11..fc300f59 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/user.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/user.dart @@ -5,6 +5,13 @@ import 'package:equatable/equatable.dart'; /// This entity corresponds to the Firebase Auth user record and acts as the /// linkage between the authentication system and the specific [Staff] or Client profiles. class User extends Equatable { + + const User({ + required this.id, + required this.email, + this.phone, + required this.role, + }); /// The unique identifier from the authentication provider (e.g., Firebase UID). final String id; @@ -18,13 +25,6 @@ class User extends Equatable { /// This determines the initial routing and permissions. final String role; - const User({ - required this.id, - required this.email, - this.phone, - required this.role, - }); - @override - List get props => [id, email, phone, role]; + List get props => [id, email, phone, role]; } \ No newline at end of file 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 4ac97077..dc353045 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 @@ -1,21 +1,46 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'data/repositories/client_create_order_repository_impl.dart'; -import 'domain/repositories/i_client_create_order_repository.dart'; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'data/repositories_impl/client_create_order_repository_impl.dart'; +import 'domain/repositories/client_create_order_repository_interface.dart'; +import 'domain/usecases/create_one_time_order_usecase.dart'; +import 'domain/usecases/create_rapid_order_usecase.dart'; import 'domain/usecases/get_order_types_usecase.dart'; import 'presentation/blocs/client_create_order_bloc.dart'; +import 'presentation/blocs/one_time_order_bloc.dart'; +import 'presentation/blocs/rapid_order_bloc.dart'; import 'presentation/pages/create_order_page.dart'; import 'presentation/pages/one_time_order_page.dart'; import 'presentation/pages/permanent_order_page.dart'; import 'presentation/pages/rapid_order_page.dart'; import 'presentation/pages/recurring_order_page.dart'; +/// Module for the Client Create Order feature. +/// +/// This module orchestrates the dependency injection for the create order feature, +/// connecting the domain use cases with their data layer implementations and +/// presentation layer BLoCs. class ClientCreateOrderModule extends Module { + @override + List get imports => [DataConnectModule()]; + @override void binds(Injector i) { - i.add(ClientCreateOrderRepositoryImpl.new); - i.add(GetOrderTypesUseCase.new); + // Repositories + i.addLazySingleton( + () => ClientCreateOrderRepositoryImpl( + orderMock: i.get()), + ); + + // UseCases + i.addLazySingleton(GetOrderTypesUseCase.new); + i.addLazySingleton(CreateOneTimeOrderUseCase.new); + i.addLazySingleton(CreateRapidOrderUseCase.new); + + // BLoCs i.addSingleton(ClientCreateOrderBloc.new); + i.add(RapidOrderBloc.new); + i.add(OneTimeOrderBloc.new); } @override diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart deleted file mode 100644 index bd179663..00000000 --- a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories/client_create_order_repository_impl.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:design_system/design_system.dart'; -import '../../domain/entities/create_order_type.dart'; -import '../../domain/repositories/i_client_create_order_repository.dart'; - -class ClientCreateOrderRepositoryImpl implements IClientCreateOrderRepository { - @override - Future> getOrderTypes() async { - // Simulating async data fetch - await Future.delayed(const Duration(milliseconds: 100)); - - return [ - const CreateOrderType( - id: 'rapid', - icon: UiIcons.zap, - titleKey: 'client_create_order.types.rapid', - descriptionKey: 'client_create_order.types.rapid_desc', - backgroundColor: UiColors.tagError, - borderColor: UiColors.destructive, - iconBackgroundColor: UiColors.tagError, - iconColor: UiColors.destructive, - textColor: UiColors.destructive, - descriptionColor: UiColors.textError, - ), - const CreateOrderType( - id: 'one-time', - icon: UiIcons.calendar, - titleKey: 'client_create_order.types.one_time', - descriptionKey: 'client_create_order.types.one_time_desc', - backgroundColor: UiColors.tagInProgress, - borderColor: UiColors.primary, - iconBackgroundColor: UiColors.tagInProgress, - iconColor: UiColors.primary, - textColor: UiColors.primary, - descriptionColor: UiColors.primary, - ), - const CreateOrderType( - id: 'recurring', - icon: UiIcons.rotateCcw, - titleKey: 'client_create_order.types.recurring', - descriptionKey: 'client_create_order.types.recurring_desc', - backgroundColor: UiColors.tagRefunded, - borderColor: UiColors.primary, - iconBackgroundColor: UiColors.tagRefunded, - iconColor: UiColors.primary, - textColor: UiColors.primary, - descriptionColor: UiColors.textSecondary, - ), - const CreateOrderType( - id: 'permanent', - icon: UiIcons.briefcase, - titleKey: 'client_create_order.types.permanent', - descriptionKey: 'client_create_order.types.permanent_desc', - backgroundColor: UiColors.tagSuccess, - borderColor: UiColors.textSuccess, - iconBackgroundColor: UiColors.tagSuccess, - iconColor: UiColors.textSuccess, - textColor: UiColors.textSuccess, - descriptionColor: UiColors.textSuccess, - ), - ]; - } -} 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 new file mode 100644 index 00000000..e0f7d843 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -0,0 +1,33 @@ +import 'package:krow_data_connect/krow_data_connect.dart' hide OrderType; +import 'package:krow_domain/krow_domain.dart'; +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. +class ClientCreateOrderRepositoryImpl + implements ClientCreateOrderRepositoryInterface { + + /// Creates a [ClientCreateOrderRepositoryImpl]. + /// + /// Requires an [OrderRepositoryMock] from the Data Connect shared package. + ClientCreateOrderRepositoryImpl({required OrderRepositoryMock orderMock}) + : _orderMock = orderMock; + final OrderRepositoryMock _orderMock; + + @override + Future> getOrderTypes() { + return _orderMock.getOrderTypes(); + } + + @override + Future createOneTimeOrder(OneTimeOrder order) { + return _orderMock.createOneTimeOrder(order); + } + + @override + Future createRapidOrder(String description) { + 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 new file mode 100644 index 00000000..08db06db --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart @@ -0,0 +1,13 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Represents the arguments required for the [CreateOneTimeOrderUseCase]. +class OneTimeOrderArguments extends UseCaseArgument { + + const OneTimeOrderArguments({required this.order}); + /// The order details to be created. + final OneTimeOrder order; + + @override + List get props => [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 new file mode 100644 index 00000000..58212905 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart @@ -0,0 +1,12 @@ +import 'package:krow_core/core.dart'; + +/// Represents the arguments required for the [CreateRapidOrderUseCase]. +class RapidOrderArguments extends UseCaseArgument { + + const RapidOrderArguments({required this.description}); + /// The text description of the urgent staffing need. + final String description; + + @override + List get props => [description]; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart deleted file mode 100644 index 2fe3d98d..00000000 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/entities/create_order_type.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/widgets.dart'; - -/// Entity representing an Order Type. -class CreateOrderType extends Equatable { - final String id; - final IconData icon; - final String titleKey; // Key for translation - final String descriptionKey; // Key for translation - final Color backgroundColor; - final Color borderColor; - final Color iconBackgroundColor; - final Color iconColor; - final Color textColor; - final Color descriptionColor; - - const CreateOrderType({ - required this.id, - required this.icon, - required this.titleKey, - required this.descriptionKey, - required this.backgroundColor, - required this.borderColor, - required this.iconBackgroundColor, - required this.iconColor, - required this.textColor, - required this.descriptionColor, - }); - - @override - List get props => [ - id, - icon, - titleKey, - descriptionKey, - backgroundColor, - borderColor, - iconBackgroundColor, - iconColor, - textColor, - descriptionColor, - ]; -} 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 new file mode 100644 index 00000000..895fdd64 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart @@ -0,0 +1,16 @@ +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.). +abstract interface class ClientCreateOrderRepositoryInterface { + /// Retrieves the list of available order types. + Future> getOrderTypes(); + + /// Submits a one-time staffing order. + Future createOneTimeOrder(OneTimeOrder order); + + /// Submits a rapid (urgent) staffing order with a text description. + Future createRapidOrder(String description); +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart deleted file mode 100644 index 4464df04..00000000 --- a/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/i_client_create_order_repository.dart +++ /dev/null @@ -1,5 +0,0 @@ -import '../entities/create_order_type.dart'; - -abstract interface class IClientCreateOrderRepository { - Future> getOrderTypes(); -} 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 new file mode 100644 index 00000000..23c92224 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart @@ -0,0 +1,20 @@ +import 'package:krow_core/core.dart'; +import '../arguments/one_time_order_arguments.dart'; +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]. +class CreateOneTimeOrderUseCase + implements UseCase { + + /// Creates a [CreateOneTimeOrderUseCase]. + const CreateOneTimeOrderUseCase(this._repository); + final ClientCreateOrderRepositoryInterface _repository; + + @override + Future call(OneTimeOrderArguments input) { + return _repository.createOneTimeOrder(input.order); + } +} 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 new file mode 100644 index 00000000..3d2d1f0c --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart @@ -0,0 +1,19 @@ +import 'package:krow_core/core.dart'; +import '../arguments/rapid_order_arguments.dart'; +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]. +class CreateRapidOrderUseCase implements UseCase { + + /// Creates a [CreateRapidOrderUseCase]. + const CreateRapidOrderUseCase(this._repository); + final ClientCreateOrderRepositoryInterface _repository; + + @override + Future call(RapidOrderArguments input) { + return _repository.createRapidOrder(input.description); + } +} 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 88037284..9473369f 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 @@ -1,12 +1,19 @@ -import '../entities/create_order_type.dart'; -import '../repositories/i_client_create_order_repository.dart'; +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/client_create_order_repository_interface.dart'; -class GetOrderTypesUseCase { - final IClientCreateOrderRepository _repository; +/// 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). +class GetOrderTypesUseCase implements NoInputUseCase> { - GetOrderTypesUseCase(this._repository); + /// Creates a [GetOrderTypesUseCase]. + const GetOrderTypesUseCase(this._repository); + final ClientCreateOrderRepositoryInterface _repository; - Future> call() { + @override + Future> call() { return _repository.getOrderTypes(); } } 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 975e73fc..794cdfd3 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 @@ -1,22 +1,24 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../../domain/usecases/get_order_types_usecase.dart'; import 'client_create_order_event.dart'; import 'client_create_order_state.dart'; +/// BLoC for managing the list of available order types. class ClientCreateOrderBloc extends Bloc { - final GetOrderTypesUseCase _getOrderTypesUseCase; ClientCreateOrderBloc(this._getOrderTypesUseCase) : super(const ClientCreateOrderInitial()) { on(_onTypesRequested); } + final GetOrderTypesUseCase _getOrderTypesUseCase; Future _onTypesRequested( ClientCreateOrderTypesRequested event, Emitter emit, ) async { - final types = await _getOrderTypesUseCase(); + final List types = await _getOrderTypesUseCase(); emit(ClientCreateOrderLoadSuccess(types)); } } 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 33ed40eb..6b16d110 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 @@ -4,7 +4,7 @@ abstract class ClientCreateOrderEvent extends Equatable { const ClientCreateOrderEvent(); @override - List get props => []; + List get props => []; } class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent { @@ -12,10 +12,10 @@ class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent { } class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent { - final String typeId; const ClientCreateOrderTypeSelected(this.typeId); + final String typeId; @override - List get props => [typeId]; + List get props => [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 f6728810..a58f89cd 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 @@ -1,23 +1,26 @@ import 'package:equatable/equatable.dart'; -import '../../domain/entities/create_order_type.dart'; -import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; +/// Base state for the [ClientCreateOrderBloc]. abstract class ClientCreateOrderState extends Equatable { const ClientCreateOrderState(); @override - List get props => []; + List get props => []; } +/// Initial state when order types haven't been loaded yet. class ClientCreateOrderInitial extends ClientCreateOrderState { const ClientCreateOrderInitial(); } +/// State representing successfully loaded order types from the repository. class ClientCreateOrderLoadSuccess extends ClientCreateOrderState { - final List orderTypes; const ClientCreateOrderLoadSuccess(this.orderTypes); + /// The list of available order types retrieved from the domain. + final List orderTypes; @override - List get props => [orderTypes]; + List get props => [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 new file mode 100644 index 00000000..8d603b10 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart @@ -0,0 +1,93 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/arguments/one_time_order_arguments.dart'; +import '../../domain/usecases/create_one_time_order_usecase.dart'; +import 'one_time_order_event.dart'; +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); + on(_onLocationChanged); + on(_onPositionAdded); + on(_onPositionRemoved); + on(_onPositionUpdated); + on(_onSubmitted); + } + final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; + + void _onDateChanged( + OneTimeOrderDateChanged event, + Emitter emit, + ) { + emit(state.copyWith(date: event.date)); + } + + void _onLocationChanged( + OneTimeOrderLocationChanged event, + Emitter emit, + ) { + emit(state.copyWith(location: event.location)); + } + + void _onPositionAdded( + OneTimeOrderPositionAdded event, + Emitter emit, + ) { + final List newPositions = + List.from(state.positions) + ..add(const OneTimeOrderPosition( + role: '', + count: 1, + startTime: '', + endTime: '', + )); + emit(state.copyWith(positions: newPositions)); + } + + void _onPositionRemoved( + OneTimeOrderPositionRemoved event, + Emitter emit, + ) { + if (state.positions.length > 1) { + final List newPositions = + List.from(state.positions) + ..removeAt(event.index); + emit(state.copyWith(positions: newPositions)); + } + } + + void _onPositionUpdated( + OneTimeOrderPositionUpdated event, + Emitter emit, + ) { + final List newPositions = + List.from(state.positions); + newPositions[event.index] = event.position; + emit(state.copyWith(positions: newPositions)); + } + + Future _onSubmitted( + OneTimeOrderSubmitted event, + Emitter emit, + ) async { + emit(state.copyWith(status: OneTimeOrderStatus.loading)); + try { + final OneTimeOrder order = OneTimeOrder( + date: state.date, + location: state.location, + positions: state.positions, + ); + await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order)); + emit(state.copyWith(status: OneTimeOrderStatus.success)); + } catch (e) { + emit(state.copyWith( + status: OneTimeOrderStatus.failure, + errorMessage: e.toString(), + )); + } + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart new file mode 100644 index 00000000..749bbb2e --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart @@ -0,0 +1,50 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +abstract class OneTimeOrderEvent extends Equatable { + const OneTimeOrderEvent(); + + @override + List get props => []; +} + +class OneTimeOrderDateChanged extends OneTimeOrderEvent { + const OneTimeOrderDateChanged(this.date); + final DateTime date; + + @override + List get props => [date]; +} + +class OneTimeOrderLocationChanged extends OneTimeOrderEvent { + const OneTimeOrderLocationChanged(this.location); + final String location; + + @override + List get props => [location]; +} + +class OneTimeOrderPositionAdded extends OneTimeOrderEvent { + const OneTimeOrderPositionAdded(); +} + +class OneTimeOrderPositionRemoved extends OneTimeOrderEvent { + const OneTimeOrderPositionRemoved(this.index); + final int index; + + @override + List get props => [index]; +} + +class OneTimeOrderPositionUpdated extends OneTimeOrderEvent { + const OneTimeOrderPositionUpdated(this.index, this.position); + final int index; + final OneTimeOrderPosition position; + + @override + List get props => [index, position]; +} + +class OneTimeOrderSubmitted extends OneTimeOrderEvent { + const OneTimeOrderSubmitted(); +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart new file mode 100644 index 00000000..2ef862f6 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +enum OneTimeOrderStatus { initial, loading, success, failure } + +class OneTimeOrderState extends Equatable { + const OneTimeOrderState({ + required this.date, + required this.location, + required this.positions, + this.status = OneTimeOrderStatus.initial, + this.errorMessage, + }); + + factory OneTimeOrderState.initial() { + return OneTimeOrderState( + date: DateTime.now(), + location: '', + positions: const [ + OneTimeOrderPosition( + role: '', + count: 1, + startTime: '', + endTime: '', + ), + ], + ); + } + final DateTime date; + final String location; + final List positions; + final OneTimeOrderStatus status; + final String? errorMessage; + + OneTimeOrderState copyWith({ + DateTime? date, + String? location, + List? positions, + OneTimeOrderStatus? status, + String? errorMessage, + }) { + return OneTimeOrderState( + date: date ?? this.date, + location: location ?? this.location, + positions: positions ?? this.positions, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [ + date, + location, + positions, + status, + errorMessage, + ]; +} 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 new file mode 100644 index 00000000..3574faf0 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart @@ -0,0 +1,89 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../domain/arguments/rapid_order_arguments.dart'; +import '../../domain/usecases/create_rapid_order_usecase.dart'; +import 'rapid_order_event.dart'; +import 'rapid_order_state.dart'; + +/// BLoC for managing the rapid (urgent) order creation flow. +class RapidOrderBloc extends Bloc { + + RapidOrderBloc(this._createRapidOrderUseCase) + : super( + const RapidOrderInitial( + examples: [ + '"We had a call out. Need 2 cooks ASAP"', + '"Need 5 bartenders ASAP until 5am"', + '"Emergency! Need 3 servers right now till midnight"', + ], + ), + ) { + on(_onMessageChanged); + on(_onVoiceToggled); + on(_onSubmitted); + on(_onExampleSelected); + } + final CreateRapidOrderUseCase _createRapidOrderUseCase; + + void _onMessageChanged( + RapidOrderMessageChanged event, + Emitter emit, + ) { + if (state is RapidOrderInitial) { + emit((state as RapidOrderInitial).copyWith(message: event.message)); + } + } + + Future _onVoiceToggled( + RapidOrderVoiceToggled event, + Emitter emit, + ) async { + if (state is RapidOrderInitial) { + final RapidOrderInitial currentState = state as RapidOrderInitial; + final bool newListeningState = !currentState.isListening; + + emit(currentState.copyWith(isListening: newListeningState)); + + // Simulate voice recognition + if (newListeningState) { + await Future.delayed(const Duration(seconds: 2)); + if (state is RapidOrderInitial) { + emit( + (state as RapidOrderInitial).copyWith( + message: 'Need 2 servers for a banquet right now.', + isListening: false, + ), + ); + } + } + } + } + + Future _onSubmitted( + RapidOrderSubmitted event, + Emitter emit, + ) async { + final RapidOrderState currentState = state; + if (currentState is RapidOrderInitial) { + final String message = currentState.message; + emit(const RapidOrderSubmitting()); + + try { + await _createRapidOrderUseCase( + RapidOrderArguments(description: message)); + emit(const RapidOrderSuccess()); + } catch (e) { + emit(RapidOrderFailure(e.toString())); + } + } + } + + void _onExampleSelected( + RapidOrderExampleSelected event, + Emitter emit, + ) { + if (state is RapidOrderInitial) { + final String cleanedExample = event.example.replaceAll('"', ''); + emit((state as RapidOrderInitial).copyWith(message: cleanedExample)); + } + } +} 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 new file mode 100644 index 00000000..b2875f77 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +abstract class RapidOrderEvent extends Equatable { + const RapidOrderEvent(); + + @override + List get props => []; +} + +class RapidOrderMessageChanged extends RapidOrderEvent { + + const RapidOrderMessageChanged(this.message); + final String message; + + @override + List get props => [message]; +} + +class RapidOrderVoiceToggled extends RapidOrderEvent { + const RapidOrderVoiceToggled(); +} + +class RapidOrderSubmitted extends RapidOrderEvent { + const RapidOrderSubmitted(); +} + +class RapidOrderExampleSelected extends RapidOrderEvent { + + const RapidOrderExampleSelected(this.example); + final String example; + + @override + List get props => [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 new file mode 100644 index 00000000..4129ed4b --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart @@ -0,0 +1,52 @@ +import 'package:equatable/equatable.dart'; + +abstract class RapidOrderState extends Equatable { + const RapidOrderState(); + + @override + List get props => []; +} + +class RapidOrderInitial extends RapidOrderState { + + const RapidOrderInitial({ + this.message = '', + this.isListening = false, + required this.examples, + }); + final String message; + final bool isListening; + final List examples; + + @override + List get props => [message, isListening, examples]; + + RapidOrderInitial copyWith({ + String? message, + bool? isListening, + List? examples, + }) { + return RapidOrderInitial( + message: message ?? this.message, + isListening: isListening ?? this.isListening, + examples: examples ?? this.examples, + ); + } +} + +class RapidOrderSubmitting extends RapidOrderState { + const RapidOrderSubmitting(); +} + +class RapidOrderSuccess extends RapidOrderState { + const RapidOrderSuccess(); +} + +class RapidOrderFailure extends RapidOrderState { + + const RapidOrderFailure(this.error); + final String error; + + @override + List get props => [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 4be2e501..b5623471 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 @@ -3,7 +3,7 @@ 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 '../../domain/entities/create_order_type.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'; @@ -11,7 +11,6 @@ import '../navigation/client_create_order_navigator.dart'; /// One-time helper to map keys to translations since they are dynamic in BLoC state String _getTranslation(String key) { - // Safe mapping - explicit keys expected if (key == 'client_create_order.types.rapid') { return t.client_create_order.types.rapid; } else if (key == 'client_create_order.types.rapid_desc') { @@ -76,7 +75,7 @@ class _CreateOrderView extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Padding( padding: const EdgeInsets.only(bottom: UiConstants.space6), child: Text( @@ -103,19 +102,22 @@ class _CreateOrderView extends StatelessWidget { ), itemCount: state.orderTypes.length, itemBuilder: (BuildContext context, int index) { - final CreateOrderType type = state.orderTypes[index]; + final OrderType type = state.orderTypes[index]; + final _OrderTypeUiMetadata ui = + _OrderTypeUiMetadata.fromId(type.id); + return _OrderTypeCard( - icon: type.icon, + icon: ui.icon, title: _getTranslation(type.titleKey), description: _getTranslation( type.descriptionKey, ), - backgroundColor: type.backgroundColor, - borderColor: type.borderColor, - iconBackgroundColor: type.iconBackgroundColor, - iconColor: type.iconColor, - textColor: type.textColor, - descriptionColor: type.descriptionColor, + backgroundColor: ui.backgroundColor, + borderColor: ui.borderColor, + iconBackgroundColor: ui.iconBackgroundColor, + iconColor: ui.iconColor, + textColor: ui.textColor, + descriptionColor: ui.descriptionColor, onTap: () { switch (type.id) { case 'rapid': @@ -149,17 +151,6 @@ class _CreateOrderView extends StatelessWidget { } class _OrderTypeCard extends StatelessWidget { - final IconData icon; - final String title; - final String description; - final Color backgroundColor; - final Color borderColor; - final Color iconBackgroundColor; - final Color iconColor; - final Color textColor; - final Color descriptionColor; - final VoidCallback onTap; - const _OrderTypeCard({ required this.icon, required this.title, @@ -173,6 +164,17 @@ class _OrderTypeCard extends StatelessWidget { required this.onTap, }); + final IconData icon; + final String title; + final String description; + final Color backgroundColor; + final Color borderColor; + final Color iconBackgroundColor; + final Color iconColor; + final Color textColor; + final Color descriptionColor; + final VoidCallback onTap; + @override Widget build(BuildContext context) { return GestureDetector( @@ -187,7 +189,7 @@ class _OrderTypeCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, - children: [ + children: [ Container( width: 48, height: 48, @@ -213,3 +215,78 @@ class _OrderTypeCard extends StatelessWidget { ); } } + +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 _OrderTypeUiMetadata.fromId(String id) { + switch (id) { + case 'rapid': + return const _OrderTypeUiMetadata( + icon: UiIcons.zap, + backgroundColor: Color(0xFFFFF7ED), + borderColor: Color(0xFFFFEDD5), + iconBackgroundColor: Color(0xFFF97316), + iconColor: Colors.white, + textColor: Color(0xFF9A3412), + descriptionColor: Color(0xFFC2410C), + ); + case 'one-time': + return const _OrderTypeUiMetadata( + icon: UiIcons.calendar, + backgroundColor: Color(0xFFF0F9FF), + borderColor: Color(0xFFE0F2FE), + iconBackgroundColor: Color(0xFF0EA5E9), + iconColor: Colors.white, + textColor: Color(0xFF075985), + descriptionColor: Color(0xFF0369A1), + ); + case 'recurring': + return const _OrderTypeUiMetadata( + icon: UiIcons.rotateCcw, + backgroundColor: Color(0xFFF0FDF4), + borderColor: Color(0xFFDCFCE7), + iconBackgroundColor: Color(0xFF22C55E), + iconColor: Colors.white, + textColor: Color(0xFF166534), + descriptionColor: Color(0xFF15803D), + ); + case 'permanent': + return const _OrderTypeUiMetadata( + icon: UiIcons.briefcase, + backgroundColor: Color(0xFFF5F3FF), + borderColor: Color(0xFFEDE9FE), + iconBackgroundColor: Color(0xFF8B5CF6), + iconColor: Colors.white, + textColor: Color(0xFF5B21B6), + descriptionColor: Color(0xFF6D28D9), + ); + default: + return const _OrderTypeUiMetadata( + icon: UiIcons.help, + backgroundColor: Colors.grey, + borderColor: Colors.grey, + iconBackgroundColor: Colors.grey, + iconColor: Colors.white, + textColor: Colors.black, + descriptionColor: Colors.black54, + ); + } + } + final IconData icon; + final Color backgroundColor; + final Color borderColor; + final Color iconBackgroundColor; + final Color iconColor; + final Color textColor; + 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 a7187324..b65bab3f 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,31 +1,648 @@ +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 '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'; +/// One-Time Order Page - Single event or shift request class OneTimeOrderPage extends StatelessWidget { const OneTimeOrderPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: - Text('One-Time Order', style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), + return BlocProvider( + create: (BuildContext context) => Modular.get(), + 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 const _SuccessView(); + } + + return Scaffold( + backgroundColor: UiColors.background, + appBar: AppBar( + title: + Text(labels.title, style: UiTypography.headline3m.textPrimary), + leading: IconButton( + icon: const Icon(UiIcons.chevronLeft, + color: UiColors.iconSecondary), + onPressed: () => Modular.to.pop(), + ), + backgroundColor: UiColors.white, + elevation: 0, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container(color: UiColors.border, height: 1.0), + ), + ), + 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: [ + _SectionHeader(title: labels.create_your_order), + const SizedBox(height: UiConstants.space4), + + // Date Picker Field + _DatePickerField( + label: labels.date_label, + value: state.date, + onChanged: (DateTime date) => + BlocProvider.of(context) + .add(OneTimeOrderDateChanged(date)), ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), + const SizedBox(height: UiConstants.space4), + + // Location Field + _LocationField( + label: labels.location_label, + value: state.location, + onChanged: (String location) => + BlocProvider.of(context) + .add(OneTimeOrderLocationChanged(location)), ), + const SizedBox(height: UiConstants.space6), + + _SectionHeader( + 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: _PositionCard( + index: index, + position: position, + isRemovable: state.positions.length > 1, + ), + ); + }), + const SizedBox(height: 100), // Space for bottom button + ], + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader({ + required this.title, + this.actionLabel, + this.onAction, + }); + final String title; + final String? actionLabel; + final VoidCallback? onAction; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: UiTypography.headline4m.textPrimary), + if (actionLabel != null && onAction != null) + TextButton.icon( + 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), + ), + ), + ], + ); + } +} + +class _DatePickerField extends StatelessWidget { + const _DatePickerField({ + required this.label, + required this.value, + required this.onChanged, + }); + final String label; + final DateTime value; + final ValueChanged onChanged; + + @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, + ), + ], + ), + ), + ), + ], + ); + } +} + +class _LocationField extends StatelessWidget { + const _LocationField({ + required this.label, + required this.value, + required this.onChanged, + }); + final String label; + final String value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: UiTypography.footnote1m.textSecondary), + const SizedBox(height: UiConstants.space2), + // Simplified for now - can use a dropdown or Autocomplete + TextField( + controller: TextEditingController(text: value) + ..selection = TextSelection.collapsed(offset: value.length), + onChanged: onChanged, + decoration: InputDecoration( + hintText: 'Select Branch/Location', + prefixIcon: const Icon(UiIcons.mapPin, + size: 20, color: UiColors.iconSecondary), + border: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + borderSide: const BorderSide(color: UiColors.border), + ), + ), + style: UiTypography.body1r.textPrimary, + ), + ], + ); + } +} + +class _PositionCard extends StatelessWidget { + const _PositionCard({ + required this.index, + required this.position, + required this.isRemovable, + }); + final int index; + final OneTimeOrderPosition position; + final bool isRemovable; + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + 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), + ), + ], ), - body: Center( - child: Text('One-Time Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${labels.positions_title} #${index + 1}', + style: UiTypography.body1b.textPrimary, + ), + if (isRemovable) + IconButton( + icon: const Icon(UiIcons.delete, + size: 20, color: UiColors.destructive), + onPressed: () => BlocProvider.of(context) + .add(OneTimeOrderPositionRemoved(index)), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + visualDensity: VisualDensity.compact, + ), + ], + ), + const Divider(height: UiConstants.space6), + + // Role (Dropdown simulation) + _LabelField( + label: labels.select_role, + child: DropdownButtonFormField( + initialValue: 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) { + BlocProvider.of(context).add( + OneTimeOrderPositionUpdated( + index, position.copyWith(role: val)), + ); + } + }, + decoration: _inputDecoration(UiIcons.briefcase), + ), + ), + const SizedBox(height: UiConstants.space4), + + // Count + _LabelField( + label: labels.workers_label, + child: Row( + children: [ + _CounterButton( + icon: UiIcons.minus, + onPressed: position.count > 1 + ? () => BlocProvider.of(context).add( + OneTimeOrderPositionUpdated(index, + position.copyWith(count: position.count - 1)), + ) + : null, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4), + child: Text('${position.count}', + style: UiTypography.headline3m.textPrimary), + ), + _CounterButton( + icon: UiIcons.add, + onPressed: () => + BlocProvider.of(context).add( + OneTimeOrderPositionUpdated( + index, position.copyWith(count: position.count + 1)), + ), + ), + ], + ), + ), + const SizedBox(height: UiConstants.space4), + + // Start/End Time + Row( + children: [ + Expanded( + child: _LabelField( + label: labels.start_label, + child: InkWell( + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 9, minute: 0), + ); + if (picked != null) { + BlocProvider.of(context).add( + OneTimeOrderPositionUpdated( + index, + 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: labels.end_label, + child: InkWell( + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 17, minute: 0), + ); + if (picked != null) { + BlocProvider.of(context).add( + OneTimeOrderPositionUpdated( + index, + 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), + + // Lunch Break + _LabelField( + label: labels.lunch_break_label, + child: DropdownButtonFormField( + initialValue: 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) { + BlocProvider.of(context).add( + OneTimeOrderPositionUpdated( + index, position.copyWith(lunchBreak: val)), + ); + } + }, + decoration: _inputDecoration(UiIcons.clock), + ), + ), + ], + ), + ); + } + + 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, + ], + ); + } +} + +class _CounterButton extends StatelessWidget { + const _CounterButton({required this.icon, this.onPressed}); + final IconData icon; + final VoidCallback? onPressed; + + @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), + ), + ), + ); + } +} + +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: ElevatedButton( + onPressed: isLoading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusLg, + ), + elevation: 0, + ), + child: isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: UiColors.white, strokeWidth: 2), + ) + : Text(label, + style: UiTypography.body1b.copyWith(color: UiColors.white)), + ), + ); + } +} + +class _SuccessView extends StatelessWidget { + const _SuccessView(); + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + + 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), + ), + const SizedBox(height: UiConstants.space8), + Text( + labels.success_title, + style: UiTypography.headline2m.textPrimary, + textAlign: TextAlign.center, + ), + const SizedBox(height: UiConstants.space4), + Text( + labels.success_message, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + const SizedBox(height: UiConstants.space10), + ElevatedButton( + onPressed: () => Modular.to.pop(), + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + minimumSize: const Size(double.infinity, 56), + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusLg, + ), + ), + child: Text('Done', + style: UiTypography.body1b.copyWith(color: UiColors.white)), + ), + ], + ), + ), ), ); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart index cdde6387..656ecdb1 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -1,17 +1,21 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; +/// Permanent Order Page - Long-term staffing placement class PermanentOrderPage extends StatelessWidget { const PermanentOrderPage({super.key}); @override Widget build(BuildContext context) { + final TranslationsClientCreateOrderPermanentEn labels = + t.client_create_order.permanent; + return Scaffold( backgroundColor: UiColors.background, appBar: AppBar( - title: - Text('Permanent Order', style: UiTypography.headline3m.textPrimary), + title: Text(labels.title, style: UiTypography.headline3m.textPrimary), leading: IconButton( icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), onPressed: () => Modular.to.pop(), @@ -24,8 +28,16 @@ class PermanentOrderPage extends StatelessWidget { ), ), body: Center( - child: Text('Permanent Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), ), ); } 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 187202d5..0a269b3f 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,30 +1,561 @@ +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'; +/// Rapid Order Flow Page - Emergency staffing requests class RapidOrderPage extends StatelessWidget { const RapidOrderPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: Text('Rapid Order', style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), + return BlocProvider( + create: (BuildContext context) => Modular.get(), + child: const _RapidOrderView(), + ); + } +} + +class _RapidOrderView extends StatelessWidget { + const _RapidOrderView(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (BuildContext context, RapidOrderState state) { + if (state is RapidOrderSuccess) { + return const _SuccessView(); + } + + 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.background, + body: Column( + children: [ + // Header with gradient + Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + UiConstants.space5, + bottom: UiConstants.space5, + left: UiConstants.space5, + right: UiConstants.space5, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.destructive, + UiColors.destructive.withValues(alpha: 0.85), + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + GestureDetector( + onTap: () => Modular.to.pop(), + 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: [ + Row( + children: [ + const Icon( + UiIcons.zap, + color: UiColors.accent, + size: 18, + ), + const SizedBox(width: UiConstants.space2), + Text( + labels.title, + style: UiTypography.headline3m.copyWith( + color: UiColors.white, + ), + ), + ], + ), + Text( + labels.subtitle, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + dateStr, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.9), + ), + ), + Text( + timeStr, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.9), + ), + ), + ], + ), + ], + ), + ), + + // 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 + 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, + ), + ), + 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 isFirst = index == 0; + + return Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space2), + child: GestureDetector( + onTap: () => + BlocProvider.of( + context) + .add( + RapidOrderExampleSelected(example), + ), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + color: isFirst + ? UiColors.accent + .withValues(alpha: 0.15) + : UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all( + color: isFirst + ? UiColors.accent + : UiColors.border, + ), + ), + child: RichText( + text: TextSpan( + style: + UiTypography.body2r.textPrimary, + children: [ + TextSpan( + text: labels.example, + style: UiTypography + .body2b.textPrimary, + ), + TextSpan(text: 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.radiusMd, + borderSide: const BorderSide( + color: UiColors.border, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: UiConstants.radiusMd, + borderSide: const BorderSide( + color: UiColors.border, + ), + ), + contentPadding: + const EdgeInsets.all(UiConstants.space4), + ), + ), + const SizedBox(height: UiConstants.space4), + + // Actions + Row( + children: [ + Expanded( + child: SizedBox( + height: 52, + child: OutlinedButton.icon( + onPressed: initialState != null + ? () => + BlocProvider.of( + context) + .add( + const RapidOrderVoiceToggled(), + ) + : null, + icon: Icon( + UiIcons + .bell, // Using bell as mic placeholder + size: 20, + color: + initialState?.isListening == true + ? UiColors.destructive + : UiColors.iconPrimary, + ), + label: Text( + initialState?.isListening == true + ? labels.listening + : labels.speak, + style: UiTypography.body2b.copyWith( + color: initialState?.isListening == + true + ? UiColors.destructive + : UiColors.textPrimary, + ), + ), + style: OutlinedButton.styleFrom( + backgroundColor: + initialState?.isListening == true + ? UiColors.destructive + .withValues(alpha: 0.05) + : UiColors.white, + side: BorderSide( + color: initialState?.isListening == + true + ? UiColors.destructive + : UiColors.border, + ), + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusMd, + ), + ), + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: SizedBox( + height: 52, + child: ElevatedButton.icon( + onPressed: isSubmitting || + (initialState?.message + .trim() + .isEmpty ?? + true) + ? null + : () => + BlocProvider.of( + context) + .add( + const RapidOrderSubmitted(), + ), + icon: const Icon( + UiIcons.arrowRight, + size: 20, + color: UiColors.white, + ), + label: Text( + isSubmitting + ? labels.sending + : labels.send, + style: UiTypography.body2b.copyWith( + color: UiColors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusMd, + ), + elevation: 0, + ), + ), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _SuccessView extends StatelessWidget { + const _SuccessView(); + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderRapidEn labels = t.client_create_order.rapid; + + return Scaffold( + body: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + UiColors.primary, + UiColors.primary.withValues(alpha: 0.85), + ], + ), + ), + child: SafeArea( + child: Center( + child: Container( + margin: + const EdgeInsets.symmetric(horizontal: UiConstants.space10), + padding: const EdgeInsets.all(UiConstants.space8), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + 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.zap, + color: UiColors.textPrimary, + size: 32, + ), + ), + ), + const SizedBox(height: UiConstants.space6), + Text( + labels.success_title, + style: UiTypography.headline1m.textPrimary, + ), + const SizedBox(height: UiConstants.space3), + Text( + labels.success_message, + textAlign: TextAlign.center, + style: UiTypography.body2r.copyWith( + color: UiColors.textSecondary, + height: 1.5, + ), + ), + const SizedBox(height: UiConstants.space8), + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: () => Modular.to.pop(), + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.textPrimary, + shape: RoundedRectangleBorder( + borderRadius: UiConstants.radiusMd, + ), + elevation: 0, + ), + child: Text( + labels.back_to_orders, + style: UiTypography.body1b.copyWith( + color: UiColors.white, + ), + ), + ), + ), + ], + ), + ), + ), ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), - ), - body: Center( - child: Text('Rapid Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), ), ); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart index 98092ea6..db437f9d 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -1,17 +1,21 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; +/// Recurring Order Page - Ongoing weekly/monthly coverage class RecurringOrderPage extends StatelessWidget { const RecurringOrderPage({super.key}); @override Widget build(BuildContext context) { + final TranslationsClientCreateOrderRecurringEn labels = + t.client_create_order.recurring; + return Scaffold( backgroundColor: UiColors.background, appBar: AppBar( - title: - Text('Recurring Order', style: UiTypography.headline3m.textPrimary), + title: Text(labels.title, style: UiTypography.headline3m.textPrimary), leading: IconButton( icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), onPressed: () => Modular.to.pop(), @@ -24,8 +28,16 @@ class RecurringOrderPage extends StatelessWidget { ), ), body: Center( - child: Text('Recurring Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), ), ); } diff --git a/apps/mobile/packages/features/client/create_order/pubspec.lock b/apps/mobile/packages/features/client/create_order/pubspec.lock index 2a4056b0..41d3237a 100644 --- a/apps/mobile/packages/features/client/create_order/pubspec.lock +++ b/apps/mobile/packages/features/client/create_order/pubspec.lock @@ -300,7 +300,7 @@ packages: source: hosted version: "4.1.2" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" @@ -324,12 +324,19 @@ packages: source: hosted version: "0.7.2" krow_core: - dependency: transitive + 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: diff --git a/apps/mobile/packages/features/client/create_order/pubspec.yaml b/apps/mobile/packages/features/client/create_order/pubspec.yaml index 34cc1051..6ff66afe 100644 --- a/apps/mobile/packages/features/client/create_order/pubspec.yaml +++ b/apps/mobile/packages/features/client/create_order/pubspec.yaml @@ -12,12 +12,17 @@ dependencies: flutter_bloc: ^8.1.3 flutter_modular: ^6.3.2 equatable: ^2.0.5 + intl: 0.20.2 design_system: path: ../../../design_system core_localization: path: ../../../core_localization krow_domain: path: ../../../domain + krow_core: + path: ../../../core + krow_data_connect: + path: ../../../data_connect dev_dependencies: flutter_test: From 96ff173855a3d4dd11e324256fbe04f873d6ae81 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 22 Jan 2026 17:03:17 -0500 Subject: [PATCH 3/3] Refactor create order UI and add widget components Refactored the client create order flow to use new modular widget components for one-time and rapid order forms, improving code organization and reusability. Updated UI colors, app bar usage, and extracted logic for date picker, location input, position card, section header, and success views. Added placeholder pages for recurring and permanent order types. Updated Spanish localization for new order types. --- .../lib/src/l10n/es.i18n.json | 10 + .../presentation/pages/create_order_page.dart | 181 ++---- .../pages/one_time_order_page.dart | 555 ++---------------- .../pages/permanent_order_page.dart | 42 +- .../presentation/pages/rapid_order_page.dart | 462 ++++----------- .../pages/recurring_order_page.dart | 42 +- .../one_time_order_date_picker.dart | 68 +++ .../one_time_order_location_input.dart | 34 ++ .../one_time_order_position_card.dart | 294 ++++++++++ .../one_time_order_section_header.dart | 42 ++ .../one_time_order_success_view.dart | 71 +++ .../presentation/widgets/order_type_card.dart | 95 +++ .../rapid_order/rapid_order_example_card.dart | 61 ++ .../rapid_order/rapid_order_header.dart | 122 ++++ .../rapid_order/rapid_order_success_view.dart | 108 ++++ 15 files changed, 1174 insertions(+), 1013 deletions(-) create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart create mode 100644 apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart 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 700570ef..d207dc0b 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,6 +287,16 @@ "creating": "Creando...", "success_title": "¡Orden Creada!", "success_message": "Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto." + }, + "recurring": { + "title": "Orden Recurrente", + "subtitle": "Cobertura continua semanal/mensual", + "placeholder": "Flujo de Orden Recurrente (Trabajo en Progreso)" + }, + "permanent": { + "title": "Orden Permanente", + "subtitle": "Colocación de personal a largo plazo", + "placeholder": "Flujo de Orden Permanente (Trabajo en Progreso)" } } } 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 b5623471..42c91202 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 @@ -8,9 +8,10 @@ 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'; -/// One-time helper to map keys to translations since they are dynamic in BLoC state -String _getTranslation(String key) { +/// 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') { @@ -31,7 +32,10 @@ String _getTranslation(String key) { return key; } +/// Main entry page for the client create order flow. +/// Allows the user to select the type of order they want to create. class ClientCreateOrderPage extends StatelessWidget { + /// Creates a [ClientCreateOrderPage]. const ClientCreateOrderPage({super.key}); @override @@ -50,22 +54,10 @@ class _CreateOrderView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - title: Text( - t.client_create_order.title, - style: UiTypography.headline3m.textPrimary, - ), + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: t.client_create_order.title, + onLeadingPressed: () => Modular.to.pop(), ), body: SafeArea( child: Padding( @@ -104,13 +96,13 @@ class _CreateOrderView extends StatelessWidget { itemBuilder: (BuildContext context, int index) { final OrderType type = state.orderTypes[index]; final _OrderTypeUiMetadata ui = - _OrderTypeUiMetadata.fromId(type.id); + _OrderTypeUiMetadata.fromId(id: type.id); - return _OrderTypeCard( + return OrderTypeCard( icon: ui.icon, - title: _getTranslation(type.titleKey), + title: _getTranslation(key: type.titleKey), description: _getTranslation( - type.descriptionKey, + key: type.descriptionKey, ), backgroundColor: ui.backgroundColor, borderColor: ui.borderColor, @@ -150,74 +142,8 @@ class _CreateOrderView extends StatelessWidget { } } -class _OrderTypeCard extends StatelessWidget { - 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, - }); - - final IconData icon; - final String title; - final String description; - final Color backgroundColor; - final Color borderColor; - final Color iconBackgroundColor; - final Color iconColor; - final Color textColor; - final Color descriptionColor; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(UiConstants.space5), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: borderColor, width: 2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - width: 48, - height: 48, - margin: const EdgeInsets.only(bottom: UiConstants.space3), - decoration: BoxDecoration( - color: iconBackgroundColor, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - child: Icon(icon, color: iconColor, size: 24), - ), - Text( - title, - style: UiTypography.body2b.copyWith(color: textColor), - ), - const SizedBox(height: UiConstants.space1), - Text( - description, - style: UiTypography.footnote1r.copyWith(color: descriptionColor), - ), - ], - ), - ), - ); - } -} - +/// Metadata for styling order type cards based on their ID. class _OrderTypeUiMetadata { - const _OrderTypeUiMetadata({ required this.icon, required this.backgroundColor, @@ -228,65 +154,80 @@ class _OrderTypeUiMetadata { required this.descriptionColor, }); - factory _OrderTypeUiMetadata.fromId(String id) { + /// 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: Color(0xFFFFF7ED), - borderColor: Color(0xFFFFEDD5), - iconBackgroundColor: Color(0xFFF97316), - iconColor: Colors.white, - textColor: Color(0xFF9A3412), - descriptionColor: Color(0xFFC2410C), + 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: Color(0xFFF0F9FF), - borderColor: Color(0xFFE0F2FE), - iconBackgroundColor: Color(0xFF0EA5E9), - iconColor: Colors.white, - textColor: Color(0xFF075985), - descriptionColor: Color(0xFF0369A1), + 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: Color(0xFFF0FDF4), - borderColor: Color(0xFFDCFCE7), - iconBackgroundColor: Color(0xFF22C55E), - iconColor: Colors.white, - textColor: Color(0xFF166534), - descriptionColor: Color(0xFF15803D), + 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: Color(0xFFF5F3FF), - borderColor: Color(0xFFEDE9FE), - iconBackgroundColor: Color(0xFF8B5CF6), - iconColor: Colors.white, - textColor: Color(0xFF5B21B6), - descriptionColor: Color(0xFF6D28D9), + 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: Colors.grey, - borderColor: Colors.grey, - iconBackgroundColor: Colors.grey, - iconColor: Colors.white, - textColor: Colors.black, - descriptionColor: Colors.black54, + 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 b65bab3f..96995b2e 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 @@ -3,14 +3,20 @@ 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 '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'; -/// One-Time Order Page - Single event or shift request +/// Page for creating a one-time staffing order. +/// Users can specify the date, location, and multiple staff positions required. class OneTimeOrderPage extends StatelessWidget { + /// Creates a [OneTimeOrderPage]. const OneTimeOrderPage({super.key}); @override @@ -33,25 +39,19 @@ class _OneTimeOrderView extends StatelessWidget { return BlocBuilder( builder: (BuildContext context, OneTimeOrderState state) { if (state.status == OneTimeOrderStatus.success) { - return const _SuccessView(); + return OneTimeOrderSuccessView( + title: labels.success_title, + message: labels.success_message, + buttonLabel: 'Done', + onDone: () => Modular.to.pop(), + ); } return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: - Text(labels.title, style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, - color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: labels.title, + onLeadingPressed: () => Modular.to.pop(), ), body: Stack( children: [ @@ -84,11 +84,10 @@ class _OneTimeOrderForm extends StatelessWidget { return ListView( padding: const EdgeInsets.all(UiConstants.space5), children: [ - _SectionHeader(title: labels.create_your_order), + OneTimeOrderSectionHeader(title: labels.create_your_order), const SizedBox(height: UiConstants.space4), - // Date Picker Field - _DatePickerField( + OneTimeOrderDatePicker( label: labels.date_label, value: state.date, onChanged: (DateTime date) => @@ -97,8 +96,7 @@ class _OneTimeOrderForm extends StatelessWidget { ), const SizedBox(height: UiConstants.space4), - // Location Field - _LocationField( + OneTimeOrderLocationInput( label: labels.location_label, value: state.location, onChanged: (String location) => @@ -107,7 +105,7 @@ class _OneTimeOrderForm extends StatelessWidget { ), const SizedBox(height: UiConstants.space6), - _SectionHeader( + OneTimeOrderSectionHeader( title: labels.positions_title, actionLabel: labels.add_position, onAction: () => BlocProvider.of(context) @@ -124,10 +122,25 @@ class _OneTimeOrderForm extends StatelessWidget { final OneTimeOrderPosition position = entry.value; return Padding( padding: const EdgeInsets.only(bottom: UiConstants.space4), - child: _PositionCard( + 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)); + }, ), ); }), @@ -137,403 +150,6 @@ class _OneTimeOrderForm extends StatelessWidget { } } -class _SectionHeader extends StatelessWidget { - const _SectionHeader({ - required this.title, - this.actionLabel, - this.onAction, - }); - final String title; - final String? actionLabel; - final VoidCallback? onAction; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: UiTypography.headline4m.textPrimary), - if (actionLabel != null && onAction != null) - TextButton.icon( - 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), - ), - ), - ], - ); - } -} - -class _DatePickerField extends StatelessWidget { - const _DatePickerField({ - required this.label, - required this.value, - required this.onChanged, - }); - final String label; - final DateTime value; - final ValueChanged onChanged; - - @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, - ), - ], - ), - ), - ), - ], - ); - } -} - -class _LocationField extends StatelessWidget { - const _LocationField({ - required this.label, - required this.value, - required this.onChanged, - }); - final String label; - final String value; - final ValueChanged onChanged; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote1m.textSecondary), - const SizedBox(height: UiConstants.space2), - // Simplified for now - can use a dropdown or Autocomplete - TextField( - controller: TextEditingController(text: value) - ..selection = TextSelection.collapsed(offset: value.length), - onChanged: onChanged, - decoration: InputDecoration( - hintText: 'Select Branch/Location', - prefixIcon: const Icon(UiIcons.mapPin, - size: 20, color: UiColors.iconSecondary), - border: OutlineInputBorder( - borderRadius: UiConstants.radiusLg, - borderSide: const BorderSide(color: UiColors.border), - ), - ), - style: UiTypography.body1r.textPrimary, - ), - ], - ); - } -} - -class _PositionCard extends StatelessWidget { - const _PositionCard({ - required this.index, - required this.position, - required this.isRemovable, - }); - final int index; - final OneTimeOrderPosition position; - final bool isRemovable; - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - 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, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${labels.positions_title} #${index + 1}', - style: UiTypography.body1b.textPrimary, - ), - if (isRemovable) - IconButton( - icon: const Icon(UiIcons.delete, - size: 20, color: UiColors.destructive), - onPressed: () => BlocProvider.of(context) - .add(OneTimeOrderPositionRemoved(index)), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - visualDensity: VisualDensity.compact, - ), - ], - ), - const Divider(height: UiConstants.space6), - - // Role (Dropdown simulation) - _LabelField( - label: labels.select_role, - child: DropdownButtonFormField( - initialValue: 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) { - BlocProvider.of(context).add( - OneTimeOrderPositionUpdated( - index, position.copyWith(role: val)), - ); - } - }, - decoration: _inputDecoration(UiIcons.briefcase), - ), - ), - const SizedBox(height: UiConstants.space4), - - // Count - _LabelField( - label: labels.workers_label, - child: Row( - children: [ - _CounterButton( - icon: UiIcons.minus, - onPressed: position.count > 1 - ? () => BlocProvider.of(context).add( - OneTimeOrderPositionUpdated(index, - position.copyWith(count: position.count - 1)), - ) - : null, - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4), - child: Text('${position.count}', - style: UiTypography.headline3m.textPrimary), - ), - _CounterButton( - icon: UiIcons.add, - onPressed: () => - BlocProvider.of(context).add( - OneTimeOrderPositionUpdated( - index, position.copyWith(count: position.count + 1)), - ), - ), - ], - ), - ), - const SizedBox(height: UiConstants.space4), - - // Start/End Time - Row( - children: [ - Expanded( - child: _LabelField( - label: labels.start_label, - child: InkWell( - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: const TimeOfDay(hour: 9, minute: 0), - ); - if (picked != null) { - BlocProvider.of(context).add( - OneTimeOrderPositionUpdated( - index, - 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: labels.end_label, - child: InkWell( - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: const TimeOfDay(hour: 17, minute: 0), - ); - if (picked != null) { - BlocProvider.of(context).add( - OneTimeOrderPositionUpdated( - index, - 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), - - // Lunch Break - _LabelField( - label: labels.lunch_break_label, - child: DropdownButtonFormField( - initialValue: 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) { - BlocProvider.of(context).add( - OneTimeOrderPositionUpdated( - index, position.copyWith(lunchBreak: val)), - ); - } - }, - decoration: _inputDecoration(UiIcons.clock), - ), - ), - ], - ), - ); - } - - 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, - ], - ); - } -} - -class _CounterButton extends StatelessWidget { - const _CounterButton({required this.icon, this.onPressed}); - final IconData icon; - final VoidCallback? onPressed; - - @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), - ), - ), - ); - } -} - class _BottomActionButton extends StatelessWidget { const _BottomActionButton({ required this.label, @@ -563,87 +179,30 @@ class _BottomActionButton extends StatelessWidget { ), ], ), - child: ElevatedButton( - onPressed: isLoading ? null : onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - minimumSize: const Size(double.infinity, 56), - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusLg, - ), - elevation: 0, - ), - child: isLoading - ? const SizedBox( + child: isLoading + ? const UiButton( + buttonBuilder: _dummyBuilder, + child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( - color: UiColors.white, strokeWidth: 2), - ) - : Text(label, - style: UiTypography.body1b.copyWith(color: UiColors.white)), - ), + color: UiColors.primary, strokeWidth: 2), + ), + ) + : UiButton.primary( + text: label, + onPressed: onPressed, + size: UiButtonSize.large, + ), ); } -} -class _SuccessView extends StatelessWidget { - const _SuccessView(); - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - - 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), - ), - const SizedBox(height: UiConstants.space8), - Text( - labels.success_title, - style: UiTypography.headline2m.textPrimary, - textAlign: TextAlign.center, - ), - const SizedBox(height: UiConstants.space4), - Text( - labels.success_message, - style: UiTypography.body1r.textSecondary, - textAlign: TextAlign.center, - ), - const SizedBox(height: UiConstants.space10), - ElevatedButton( - onPressed: () => Modular.to.pop(), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - minimumSize: const Size(double.infinity, 56), - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusLg, - ), - ), - child: Text('Done', - style: UiTypography.body1b.copyWith(color: UiColors.white)), - ), - ], - ), - ), - ), - ); + 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/permanent_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart index 656ecdb1..fd38a142 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -3,8 +3,10 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; -/// Permanent Order Page - Long-term staffing placement +/// Permanent Order Page - Long-term staffing placement. +/// Placeholder for future implementation. class PermanentOrderPage extends StatelessWidget { + /// Creates a [PermanentOrderPage]. const PermanentOrderPage({super.key}); @override @@ -13,30 +15,24 @@ class PermanentOrderPage extends StatelessWidget { t.client_create_order.permanent; return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: Text(labels.title, style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: labels.title, + onLeadingPressed: () => Modular.to.pop(), ), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - labels.subtitle, - style: UiTypography.body1r.textSecondary, - textAlign: TextAlign.center, - ), - ], + child: Padding( + padding: const EdgeInsets.all(UiConstants.space6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), ), ), ); 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 0a269b3f..0f0bb874 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 @@ -7,9 +7,14 @@ 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'; -/// Rapid Order Flow Page - Emergency staffing requests +/// Rapid Order Flow Page - Emergency staffing requests. +/// Features voice recognition simulation and quick example selection. class RapidOrderPage extends StatelessWidget { + /// Creates a [RapidOrderPage]. const RapidOrderPage({super.key}); @override @@ -26,10 +31,18 @@ class _RapidOrderView extends StatelessWidget { @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 const _SuccessView(); + return RapidOrderSuccessView( + title: labels.success_title, + message: labels.success_message, + buttonLabel: labels.back_to_orders, + onDone: () => Modular.to.pop(), + ); } return const _RapidOrderForm(); @@ -56,7 +69,8 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { @override Widget build(BuildContext context) { - final TranslationsClientCreateOrderRapidEn labels = t.client_create_order.rapid; + 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); @@ -73,97 +87,15 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { } }, child: Scaffold( - backgroundColor: UiColors.background, + backgroundColor: UiColors.bgPrimary, body: Column( children: [ - // Header with gradient - Container( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + UiConstants.space5, - bottom: UiConstants.space5, - left: UiConstants.space5, - right: UiConstants.space5, - ), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - UiColors.destructive, - UiColors.destructive.withValues(alpha: 0.85), - ], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - GestureDetector( - onTap: () => Modular.to.pop(), - 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: [ - Row( - children: [ - const Icon( - UiIcons.zap, - color: UiColors.accent, - size: 18, - ), - const SizedBox(width: UiConstants.space2), - Text( - labels.title, - style: UiTypography.headline3m.copyWith( - color: UiColors.white, - ), - ), - ], - ), - Text( - labels.subtitle, - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.8), - ), - ), - ], - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - dateStr, - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.9), - ), - ), - Text( - timeStr, - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.9), - ), - ), - ], - ), - ], - ), + RapidOrderHeader( + title: labels.title, + subtitle: labels.subtitle, + date: dateStr, + time: timeStr, + onBack: () => Modular.to.pop(), ), // Content @@ -212,40 +144,13 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { builder: (BuildContext context, RapidOrderState state) { final RapidOrderInitial? initialState = state is RapidOrderInitial ? state : null; - final bool isSubmitting = state is RapidOrderSubmitting; + final bool isSubmitting = + state is RapidOrderSubmitting; return Column( children: [ // Icon - 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, - ), - ), + _AnimatedZapIcon(), const SizedBox(height: UiConstants.space4), Text( labels.need_staff, @@ -267,51 +172,21 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { .map((MapEntry entry) { final int index = entry.key; final String example = entry.value; - final bool isFirst = index == 0; + final bool isHighlighted = index == 0; return Padding( padding: const EdgeInsets.only( bottom: UiConstants.space2), - child: GestureDetector( + child: RapidOrderExampleCard( + example: example, + isHighlighted: isHighlighted, + label: labels.example, onTap: () => BlocProvider.of( context) .add( RapidOrderExampleSelected(example), ), - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space4, - vertical: UiConstants.space3, - ), - decoration: BoxDecoration( - color: isFirst - ? UiColors.accent - .withValues(alpha: 0.15) - : UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all( - color: isFirst - ? UiColors.accent - : UiColors.border, - ), - ), - child: RichText( - text: TextSpan( - style: - UiTypography.body2r.textPrimary, - children: [ - TextSpan( - text: labels.example, - style: UiTypography - .body2b.textPrimary, - ), - TextSpan(text: example), - ], - ), - ), - ), ), ); }), @@ -332,13 +207,7 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { color: UiColors.textPlaceholder, ), border: OutlineInputBorder( - borderRadius: UiConstants.radiusMd, - borderSide: const BorderSide( - color: UiColors.border, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: UiConstants.radiusMd, + borderRadius: UiConstants.radiusLg, borderSide: const BorderSide( color: UiColors.border, ), @@ -350,100 +219,12 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { const SizedBox(height: UiConstants.space4), // Actions - Row( - children: [ - Expanded( - child: SizedBox( - height: 52, - child: OutlinedButton.icon( - onPressed: initialState != null - ? () => - BlocProvider.of( - context) - .add( - const RapidOrderVoiceToggled(), - ) - : null, - icon: Icon( - UiIcons - .bell, // Using bell as mic placeholder - size: 20, - color: - initialState?.isListening == true - ? UiColors.destructive - : UiColors.iconPrimary, - ), - label: Text( - initialState?.isListening == true - ? labels.listening - : labels.speak, - style: UiTypography.body2b.copyWith( - color: initialState?.isListening == - true - ? UiColors.destructive - : UiColors.textPrimary, - ), - ), - style: OutlinedButton.styleFrom( - backgroundColor: - initialState?.isListening == true - ? UiColors.destructive - .withValues(alpha: 0.05) - : UiColors.white, - side: BorderSide( - color: initialState?.isListening == - true - ? UiColors.destructive - : UiColors.border, - ), - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusMd, - ), - ), - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: SizedBox( - height: 52, - child: ElevatedButton.icon( - onPressed: isSubmitting || - (initialState?.message - .trim() - .isEmpty ?? - true) - ? null - : () => - BlocProvider.of( - context) - .add( - const RapidOrderSubmitted(), - ), - icon: const Icon( - UiIcons.arrowRight, - size: 20, - color: UiColors.white, - ), - label: Text( - isSubmitting - ? labels.sending - : labels.send, - style: UiTypography.body2b.copyWith( - color: UiColors.white, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusMd, - ), - elevation: 0, - ), - ), - ), - ), - ], + _RapidOrderActions( + labels: labels, + isSubmitting: isSubmitting, + isListening: initialState?.isListening ?? false, + isMessageEmpty: initialState != null && + initialState.message.trim().isEmpty, ), ], ); @@ -461,102 +242,85 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { } } -class _SuccessView extends StatelessWidget { - const _SuccessView(); - +class _AnimatedZapIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final TranslationsClientCreateOrderRapidEn labels = t.client_create_order.rapid; - - return Scaffold( - body: Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - UiColors.primary, - UiColors.primary.withValues(alpha: 0.85), - ], - ), + 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, ), - child: SafeArea( - child: Center( - child: Container( - margin: - const EdgeInsets.symmetric(horizontal: UiConstants.space10), - padding: const EdgeInsets.all(UiConstants.space8), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.2), - blurRadius: 20, - offset: const Offset(0, 10), - ), - ], - ), - 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.zap, - color: UiColors.textPrimary, - size: 32, - ), - ), - ), - const SizedBox(height: UiConstants.space6), - Text( - labels.success_title, - style: UiTypography.headline1m.textPrimary, - ), - const SizedBox(height: UiConstants.space3), - Text( - labels.success_message, - textAlign: TextAlign.center, - style: UiTypography.body2r.copyWith( - color: UiColors.textSecondary, - height: 1.5, - ), - ), - const SizedBox(height: UiConstants.space8), - SizedBox( - width: double.infinity, - height: 52, - child: ElevatedButton( - onPressed: () => Modular.to.pop(), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.textPrimary, - shape: RoundedRectangleBorder( - borderRadius: UiConstants.radiusMd, - ), - elevation: 0, - ), - child: Text( - labels.back_to_orders, - style: UiTypography.body1b.copyWith( - color: UiColors.white, - ), - ), - ), - ), - ], - ), - ), + borderRadius: UiConstants.radiusLg, + boxShadow: [ + BoxShadow( + color: UiColors.destructive.withValues(alpha: 0.3), + blurRadius: 10, + offset: const Offset(0, 4), ), - ), + ], + ), + child: const Icon( + UiIcons.zap, + color: UiColors.white, + size: 32, ), ); } } + +class _RapidOrderActions extends StatelessWidget { + const _RapidOrderActions({ + required this.labels, + required this.isSubmitting, + required this.isListening, + required this.isMessageEmpty, + }); + final TranslationsClientCreateOrderRapidEn labels; + final bool isSubmitting; + final bool isListening; + final bool isMessageEmpty; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: UiButton.secondary( + text: isListening ? labels.listening : labels.speak, + leadingIcon: UiIcons.bell, // Placeholder for mic + onPressed: () => BlocProvider.of(context).add( + const RapidOrderVoiceToggled(), + ), + style: OutlinedButton.styleFrom( + backgroundColor: isListening + ? UiColors.destructive.withValues(alpha: 0.05) + : null, + side: isListening + ? const BorderSide(color: UiColors.destructive) + : null, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: UiButton.primary( + text: isSubmitting ? labels.sending : labels.send, + trailingIcon: UiIcons.arrowRight, + onPressed: isSubmitting || isMessageEmpty + ? null + : () => BlocProvider.of(context).add( + const RapidOrderSubmitted(), + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart index db437f9d..64324b46 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -3,8 +3,10 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; -/// Recurring Order Page - Ongoing weekly/monthly coverage +/// Recurring Order Page - Ongoing weekly/monthly coverage. +/// Placeholder for future implementation. class RecurringOrderPage extends StatelessWidget { + /// Creates a [RecurringOrderPage]. const RecurringOrderPage({super.key}); @override @@ -13,30 +15,24 @@ class RecurringOrderPage extends StatelessWidget { t.client_create_order.recurring; return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: Text(labels.title, style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: labels.title, + onLeadingPressed: () => Modular.to.pop(), ), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - labels.subtitle, - style: UiTypography.body1r.textSecondary, - textAlign: TextAlign.center, - ), - ], + child: Padding( + padding: const EdgeInsets.all(UiConstants.space6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), ), ), ); 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 new file mode 100644 index 00000000..5b32274d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart @@ -0,0 +1,68 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// A date picker field for the one-time order form. +class OneTimeOrderDatePicker extends StatelessWidget { + /// The label text to display above the field. + final String label; + + /// The currently selected date. + final DateTime value; + + /// 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 + 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, + ), + ], + ), + ), + ), + ], + ); + } +} 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 new file mode 100644 index 00000000..3f93da9d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart @@ -0,0 +1,34 @@ +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; + + /// Creates a [OneTimeOrderLocationInput]. + const OneTimeOrderLocationInput({ + required this.label, + required this.value, + required this.onChanged, + super.key, + }); + + @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, + 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 new file mode 100644 index 00000000..a605ea5c --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart @@ -0,0 +1,294 @@ +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. +class OneTimeOrderPositionCard extends StatelessWidget { + /// The index of the position in the list. + final int index; + + /// The position entity data. + final OneTimeOrderPosition position; + + /// Whether this position can be removed (usually if there's more than one). + final bool isRemovable; + + /// Callback when the position data is updated. + final ValueChanged onUpdated; + + /// Callback when the position is removed. + final VoidCallback onRemoved; + + /// Label for positions (e.g., "Position"). + final String positionLabel; + + /// Label for the role selection. + final String roleLabel; + + /// Label for the worker count. + final String workersLabel; + + /// Label for the start time. + final String startLabel; + + /// Label for the end time. + final String endLabel; + + /// 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( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + 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, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$positionLabel #${index + 1}', + style: UiTypography.body1b.textPrimary, + ), + if (isRemovable) + IconButton( + icon: const Icon(UiIcons.delete, + size: 20, color: UiColors.destructive), + onPressed: onRemoved, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + visualDensity: VisualDensity.compact, + ), + ], + ), + const Divider(height: UiConstants.space6), + + // 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), + ), + ), + const SizedBox(height: UiConstants.space4), + + // Count (Counter) + _LabelField( + label: workersLabel, + child: Row( + children: [ + _CounterButton( + icon: UiIcons.minus, + onPressed: position.count > 1 + ? () => onUpdated( + position.copyWith(count: position.count - 1)) + : null, + ), + 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.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), + + // 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), + ), + ), + ], + ), + ); + } + + 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, + ], + ); + } +} + +class _CounterButton extends StatelessWidget { + const _CounterButton({required this.icon, this.onPressed}); + final IconData icon; + final VoidCallback? onPressed; + + @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), + ), + ), + ); + } +} 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 new file mode 100644 index 00000000..29c8df31 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart @@ -0,0 +1,42 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A header widget for sections in the one-time order form. +class OneTimeOrderSectionHeader extends StatelessWidget { + /// The title text for the section. + final String title; + + /// Optional label for an action button on the right. + final String? actionLabel; + + /// 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( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: UiTypography.headline4m.textPrimary), + if (actionLabel != null && onAction != null) + TextButton.icon( + 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), + ), + ), + ], + ); + } +} 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 new file mode 100644 index 00000000..ea704758 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart @@ -0,0 +1,71 @@ +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. +class OneTimeOrderSuccessView extends StatelessWidget { + /// The title of the success message. + final String title; + + /// The body of the success message. + final String message; + + /// Label for the completion button. + final String buttonLabel; + + /// 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), + ), + const SizedBox(height: UiConstants.space8), + Text( + title, + style: UiTypography.headline2m.textPrimary, + textAlign: TextAlign.center, + ), + 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/order_type_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart new file mode 100644 index 00000000..8b450b99 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart @@ -0,0 +1,95 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A card widget representing an order type in the creation flow. +class OrderTypeCard extends StatelessWidget { + /// Icon to display at the top of the card. + final IconData icon; + + /// Main title of the order type. + final String title; + + /// Brief description of what this order type entails. + final String description; + + /// Background color of the card. + final Color backgroundColor; + + /// Color of the card's border. + final Color borderColor; + + /// Background color for the icon container. + final Color iconBackgroundColor; + + /// Color of the icon itself. + final Color iconColor; + + /// Color of the title text. + final Color textColor; + + /// Color of the description text. + final Color descriptionColor; + + /// 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( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(UiConstants.space5), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: Border.all(color: borderColor, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + margin: const EdgeInsets.only(bottom: UiConstants.space3), + decoration: BoxDecoration( + color: iconBackgroundColor, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Icon(icon, color: iconColor, size: 24), + ), + Text( + title, + style: UiTypography.body2b.copyWith(color: textColor), + ), + const SizedBox(height: UiConstants.space1), + Expanded( + child: Text( + description, + style: + UiTypography.footnote1r.copyWith(color: descriptionColor), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } +} 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 new file mode 100644 index 00000000..3bde4479 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart @@ -0,0 +1,61 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A card displaying an example message for a rapid order. +class RapidOrderExampleCard extends StatelessWidget { + /// The example text. + final String example; + + /// Whether this is the first (highlighted) example. + final bool isHighlighted; + + /// The label for the example prefix (e.g., "Example:"). + final String label; + + /// 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( + onTap: onTap, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + color: isHighlighted + ? UiColors.accent.withValues(alpha: 0.15) + : UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all( + color: isHighlighted ? UiColors.accent : UiColors.border, + ), + ), + child: RichText( + text: TextSpan( + style: UiTypography.body2r.textPrimary, + children: [ + TextSpan( + text: label, + style: UiTypography.body2b.textPrimary, + ), + TextSpan(text: ' $example'), + ], + ), + ), + ), + ); + } +} 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 new file mode 100644 index 00000000..4d7a3848 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart @@ -0,0 +1,122 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A header widget for the rapid order flow with a gradient background. +class RapidOrderHeader extends StatelessWidget { + /// The title of the page. + final String title; + + /// The subtitle or description. + final String subtitle; + + /// The formatted current date. + final String date; + + /// The formatted current time. + final String time; + + /// 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( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + UiConstants.space5, + bottom: UiConstants.space5, + left: UiConstants.space5, + right: UiConstants.space5, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.destructive, + UiColors.destructive.withValues(alpha: 0.85), + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 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: [ + Row( + children: [ + const Icon( + UiIcons.zap, + color: UiColors.accent, + size: 18, + ), + const SizedBox(width: UiConstants.space2), + Text( + title, + style: UiTypography.headline3m.copyWith( + color: UiColors.white, + ), + ), + ], + ), + Text( + subtitle, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + date, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.9), + ), + ), + Text( + time, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.9), + ), + ), + ], + ), + ], + ), + ); + } +} 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 new file mode 100644 index 00000000..3ea9ad4d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart @@ -0,0 +1,108 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A view to display when a rapid order has been successfully created. +class RapidOrderSuccessView extends StatelessWidget { + /// The title of the success message. + final String title; + + /// The body of the success message. + final String message; + + /// Label for the completion button. + final String buttonLabel; + + /// 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( + body: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + UiColors.primary, + UiColors.primary.withValues(alpha: 0.85), + ], + ), + ), + child: SafeArea( + child: Center( + child: Container( + margin: + const EdgeInsets.symmetric(horizontal: UiConstants.space10), + padding: const EdgeInsets.all(UiConstants.space8), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + 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.zap, + color: UiColors.textPrimary, + size: 32, + ), + ), + ), + const SizedBox(height: UiConstants.space6), + Text( + title, + style: UiTypography.headline1m.textPrimary, + ), + const SizedBox(height: UiConstants.space3), + Text( + message, + textAlign: TextAlign.center, + style: UiTypography.body2r.copyWith( + color: UiColors.textSecondary, + height: 1.5, + ), + ), + const SizedBox(height: UiConstants.space8), + SizedBox( + width: double.infinity, + child: UiButton.primary( + text: buttonLabel, + onPressed: onDone, + size: UiButtonSize.large, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +}