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 0d5db935..f97c1777 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 @@ -311,6 +311,35 @@ "orders": "Orders", "reports": "Reports" } + }, + "client_view_orders": { + "title": "Orders", + "post_button": "Post", + "post_order": "Post an Order", + "no_orders": "No orders for $date", + "tabs": { + "up_next": "Up Next", + "active": "Active", + "completed": "Completed" + }, + "card": { + "open": "OPEN", + "filled": "FILLED", + "confirmed": "CONFIRMED", + "in_progress": "IN PROGRESS", + "completed": "COMPLETED", + "cancelled": "CANCELLED", + "get_direction": "Get direction", + "total": "Total", + "hrs": "HRS", + "workers": "workers", + "clock_in": "CLOCK IN", + "clock_out": "CLOCK OUT", + "coverage": "Coverage", + "workers_label": "$filled/$needed Workers", + "confirmed_workers": "Workers Confirmed", + "no_workers": "No workers confirmed yet." + } } } 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 fcabb08d..c141a406 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 @@ -311,5 +311,34 @@ "orders": "Órdenes", "reports": "Reportes" } + }, + "client_view_orders": { + "title": "Órdenes", + "post_button": "Publicar", + "post_order": "Publicar una Orden", + "no_orders": "No hay órdenes para $date", + "tabs": { + "up_next": "Próximos", + "active": "Activos", + "completed": "Completados" + }, + "card": { + "open": "ABIERTO", + "filled": "LLENO", + "confirmed": "CONFIRMADO", + "in_progress": "EN PROGRESO", + "completed": "COMPLETADO", + "cancelled": "CANCELADO", + "get_direction": "Obtener dirección", + "total": "Total", + "hrs": "HRS", + "workers": "trabajadores", + "clock_in": "ENTRADA", + "clock_out": "SALIDA", + "coverage": "Cobertura", + "workers_label": "$filled/$needed Trabajadores", + "confirmed_workers": "Trabajadores Confirmados", + "no_workers": "Ningún trabajador confirmado aún." + } } } 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 index 8e7979ea..95c6f025 100644 --- 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 @@ -7,7 +7,7 @@ import 'package:krow_domain/krow_domain.dart'; class OrderRepositoryMock { /// Returns a list of available [OrderType]s. Future> getOrderTypes() async { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); return const [ OrderType( id: 'rapid', @@ -34,11 +34,122 @@ class OrderRepositoryMock { /// Simulates creating a one-time order. Future createOneTimeOrder(OneTimeOrder order) async { - await Future.delayed(const Duration(milliseconds: 800)); + await Future.delayed(const Duration(milliseconds: 800)); } /// Simulates creating a rapid order. Future createRapidOrder(String description) async { - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(seconds: 1)); + } + + /// Returns a mock list of client orders. + Future> getOrders() async { + await Future.delayed(const Duration(milliseconds: 500)); + return [ + OrderItem( + id: '1', + title: 'Server - Wedding', + clientName: 'Grand Plaza Hotel', + status: 'filled', + date: DateTime.now() + .add(const Duration(days: 1)) + .toIso8601String() + .split('T')[0], + startTime: '16:00', + endTime: '23:00', + location: 'Grand Plaza Hotel, 123 Main St', + locationAddress: 'Grand Plaza Hotel, 123 Main St', + filled: 10, + workersNeeded: 10, + hourlyRate: 22.0, + confirmedApps: List>.generate( + 10, + (int index) => { + 'id': 'app_$index', + 'worker_id': 'w_$index', + 'worker_name': 'Worker ${String.fromCharCode(65 + index)}', + 'status': 'confirmed', + 'check_in_time': index < 5 ? '15:55' : null, + }, + ), + ), + OrderItem( + id: '2', + title: 'Bartender - Private Event', + clientName: 'Taste of the Town', + status: 'open', + date: DateTime.now() + .add(const Duration(days: 1)) + .toIso8601String() + .split('T')[0], + startTime: '18:00', + endTime: '02:00', + location: 'Downtown Loft, 456 High St', + locationAddress: 'Downtown Loft, 456 High St', + filled: 4, + workersNeeded: 5, + hourlyRate: 28.0, + confirmedApps: List>.generate( + 4, + (int index) => { + 'id': 'app_b_$index', + 'worker_id': 'w_b_$index', + 'worker_name': 'Bartender ${index + 1}', + 'status': 'confirmed', + }, + ), + ), + OrderItem( + id: '3', + title: 'Event Staff', + clientName: 'City Center', + status: 'in_progress', + date: DateTime.now().toIso8601String().split('T')[0], + startTime: '08:00', + endTime: '16:00', + location: 'Convention Center, 789 Blvd', + locationAddress: 'Convention Center, 789 Blvd', + filled: 15, + workersNeeded: 15, + hourlyRate: 20.0, + confirmedApps: List>.generate( + 15, + (int index) => { + 'id': 'app_c_$index', + 'worker_id': 'w_c_$index', + 'worker_name': 'Staff ${index + 1}', + 'status': 'confirmed', + 'check_in_time': '07:55', + }, + ), + ), + OrderItem( + id: '4', + title: 'Coat Check', + clientName: 'The Met Museum', + status: 'completed', + date: DateTime.now() + .subtract(const Duration(days: 1)) + .toIso8601String() + .split('T')[0], + startTime: '17:00', + endTime: '22:00', + location: 'The Met Museum, 1000 5th Ave', + locationAddress: 'The Met Museum, 1000 5th Ave', + filled: 2, + workersNeeded: 2, + hourlyRate: 18.0, + confirmedApps: List>.generate( + 2, + (int index) => { + 'id': 'app_d_$index', + 'worker_id': 'w_d_$index', + 'worker_name': 'Checker ${index + 1}', + 'status': 'confirmed', + 'check_in_time': '16:50', + }, + ), + ), + ]; } } 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 c24c5140..6b04f468 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_icons.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_icons.dart @@ -34,6 +34,9 @@ class UiIcons { /// User icon for profile static const IconData user = _IconLib.user; + /// Users icon for groups or staff + static const IconData users = _IconLib.users; + /// Settings icon static const IconData settings = _IconLib.settings; @@ -81,6 +84,9 @@ class UiIcons { /// Chevron down icon static const IconData chevronDown = _IconLib.chevronDown; + /// Chevron up icon + static const IconData chevronUp = _IconLib.chevronUp; + // --- Status & Feedback --- /// Info icon @@ -139,6 +145,9 @@ class UiIcons { /// Sparkles icon for features or AI static const IconData sparkles = _IconLib.sparkles; + /// Navigation/Compass icon + static const IconData navigation = _IconLib.navigation; + /// Star icon for ratings static const IconData star = _IconLib.star; @@ -154,6 +163,12 @@ class UiIcons { /// Eye off icon for hidden visibility static const IconData eyeOff = _IconLib.eyeOff; + /// Phone icon for calls + static const IconData phone = _IconLib.phone; + + /// Message circle icon for chat + static const IconData messageCircle = _IconLib.messageCircle; + /// Building icon for companies static const IconData building = _IconLib.building2; diff --git a/apps/mobile/packages/design_system/lib/src/ui_theme.dart b/apps/mobile/packages/design_system/lib/src/ui_theme.dart index 47252d81..92e295b2 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_theme.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_theme.dart @@ -12,8 +12,8 @@ class UiTheme { /// Returns the light theme for the Staff application. static ThemeData get light { - final colorScheme = UiColors.colorScheme; - final textTheme = UiTypography.textTheme; + final ColorScheme colorScheme = UiColors.colorScheme; + final TextTheme textTheme = UiTypography.textTheme; return ThemeData( useMaterial3: true, @@ -68,7 +68,6 @@ class UiTheme { horizontal: UiConstants.space6, vertical: UiConstants.space3, ), - minimumSize: const Size(double.infinity, 54), maximumSize: const Size(double.infinity, 54), ).copyWith( side: WidgetStateProperty.resolveWith((states) { @@ -99,7 +98,6 @@ class UiTheme { horizontal: UiConstants.space4, vertical: UiConstants.space2, ), - minimumSize: const Size(double.infinity, 52), maximumSize: const Size(double.infinity, 52), ), ), @@ -117,7 +115,6 @@ class UiTheme { horizontal: UiConstants.space4, vertical: UiConstants.space3, ), - minimumSize: const Size(double.infinity, 52), maximumSize: const Size(double.infinity, 52), ), ), diff --git a/apps/mobile/packages/design_system/lib/src/ui_typography.dart b/apps/mobile/packages/design_system/lib/src/ui_typography.dart index 9f3d5b99..dc795923 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_typography.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_typography.dart @@ -320,6 +320,15 @@ class UiTypography { color: UiColors.textPrimary, ); + /// Body 3 Medium - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: -0.1 (#121826) + static final TextStyle body3m = _primaryBase.copyWith( + fontWeight: FontWeight.w500, + fontSize: 12, + height: 1.5, + letterSpacing: -0.1, + color: UiColors.textPrimary, + ); + /// Body 4 Regular - Font: Instrument Sans, Size: 14, Height: 1.5, Spacing: 0.05 (#121826) static final TextStyle body4r = _primaryBase.copyWith( fontWeight: FontWeight.w400, diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart index 4f0535c6..1460f07a 100644 --- a/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_button.dart @@ -27,6 +27,9 @@ class UiButton extends StatelessWidget { /// The size of the button. final UiButtonSize size; + /// Whether the button should take up the full width of its container. + final bool fullWidth; + /// The button widget to use (ElevatedButton, OutlinedButton, or TextButton). final Widget Function( BuildContext context, @@ -48,6 +51,7 @@ class UiButton extends StatelessWidget { this.style, this.iconSize = 20, this.size = UiButtonSize.medium, + this.fullWidth = false, }) : assert( text != null || child != null, 'Either text or child must be provided', @@ -64,6 +68,7 @@ class UiButton extends StatelessWidget { this.style, this.iconSize = 20, this.size = UiButtonSize.medium, + this.fullWidth = false, }) : buttonBuilder = _elevatedButtonBuilder, assert( text != null || child != null, @@ -81,6 +86,7 @@ class UiButton extends StatelessWidget { this.style, this.iconSize = 20, this.size = UiButtonSize.medium, + this.fullWidth = false, }) : buttonBuilder = _outlinedButtonBuilder, assert( text != null || child != null, @@ -98,6 +104,25 @@ class UiButton extends StatelessWidget { this.style, this.iconSize = 20, this.size = UiButtonSize.medium, + this.fullWidth = false, + }) : buttonBuilder = _textButtonBuilder, + assert( + text != null || child != null, + 'Either text or child must be provided', + ); + + /// Creates a ghost button (transparent background). + UiButton.ghost({ + super.key, + this.text, + this.child, + this.onPressed, + this.leadingIcon, + this.trailingIcon, + this.style, + this.iconSize = 20, + this.size = UiButtonSize.medium, + this.fullWidth = false, }) : buttonBuilder = _textButtonBuilder, assert( text != null || child != null, @@ -107,7 +132,18 @@ class UiButton extends StatelessWidget { @override /// Builds the button UI. Widget build(BuildContext context) { - return buttonBuilder(context, onPressed, style, _buildButtonContent()); + final Widget button = buttonBuilder( + context, + onPressed, + style, + _buildButtonContent(), + ); + + if (fullWidth) { + return SizedBox(width: double.infinity, child: button); + } + + return button; } /// Builds the button content with optional leading and trailing icons. @@ -116,27 +152,40 @@ class UiButton extends StatelessWidget { return child!; } - // Single icon or text case + final String buttonText = text ?? ''; + + // Optimization: If no icons, return plain text to avoid Row layout overhead if (leadingIcon == null && trailingIcon == null) { - return Text(text!); + return Text(buttonText, textAlign: TextAlign.center); } - if (leadingIcon != null && text == null && trailingIcon == null) { - return Icon(leadingIcon, size: iconSize); - } - - // Multiple elements case + // Multiple elements case: Use a Row with MainAxisSize.min final List children = []; if (leadingIcon != null) { children.add(Icon(leadingIcon, size: iconSize)); - children.add(const SizedBox(width: UiConstants.space2)); } - children.add(Text(text!)); + if (buttonText.isNotEmpty) { + if (leadingIcon != null) { + children.add(const SizedBox(width: UiConstants.space2)); + } + // Use flexible to ensure text doesn't force infinite width in flex parents + children.add( + Flexible( + child: Text( + buttonText, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } if (trailingIcon != null) { - children.add(const SizedBox(width: UiConstants.space2)); + if (buttonText.isNotEmpty || leadingIcon != null) { + children.add(const SizedBox(width: UiConstants.space2)); + } children.add(Icon(trailingIcon, size: iconSize)); } diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 07c99633..aead3421 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -19,6 +19,7 @@ export 'src/entities/business/business_setting.dart'; export 'src/entities/business/hub.dart'; export 'src/entities/business/hub_department.dart'; export 'src/entities/business/biz_contract.dart'; +export 'src/entities/business/vendor.dart'; // Events & Shifts export 'src/entities/events/event.dart'; @@ -31,6 +32,7 @@ export 'src/entities/events/work_session.dart'; export 'src/entities/orders/order_type.dart'; export 'src/entities/orders/one_time_order.dart'; export 'src/entities/orders/one_time_order_position.dart'; +export 'src/entities/orders/order_item.dart'; // Skills & Certs export 'src/entities/skills/skill.dart'; diff --git a/apps/mobile/packages/domain/lib/src/entities/business/vendor.dart b/apps/mobile/packages/domain/lib/src/entities/business/vendor.dart new file mode 100644 index 00000000..19d8bf98 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/business/vendor.dart @@ -0,0 +1,15 @@ +import 'package:equatable/equatable.dart'; + +/// Represents a staffing vendor. +class Vendor extends Equatable { + const Vendor({required this.id, required this.name, required this.rates}); + + final String id; + final String name; + + /// A map of role names to hourly rates. + final Map rates; + + @override + List get props => [id, name, rates]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart b/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart new file mode 100644 index 00000000..6950c7b6 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/order_item.dart @@ -0,0 +1,80 @@ +import 'package:equatable/equatable.dart'; + +/// Represents a customer's view of an order or shift. +/// +/// This entity captures the details necessary for the dashboard/view orders screen, +/// including status and worker assignments. +class OrderItem extends Equatable { + /// Creates an [OrderItem]. + const OrderItem({ + required this.id, + required this.title, + required this.clientName, + required this.status, + required this.date, + required this.startTime, + required this.endTime, + required this.location, + required this.locationAddress, + required this.filled, + required this.workersNeeded, + required this.hourlyRate, + this.confirmedApps = const >[], + }); + + /// Unique identifier of the order. + final String id; + + /// Title or name of the role. + final String title; + + /// Name of the client company. + final String clientName; + + /// status of the order (e.g., 'open', 'filled', 'completed'). + final String status; + + /// Date of the shift (ISO format). + final String date; + + /// Start time of the shift. + final String startTime; + + /// End time of the shift. + final String endTime; + + /// Location name. + final String location; + + /// Full address of the location. + final String locationAddress; + + /// Number of workers currently filled. + final int filled; + + /// Total number of workers required. + final int workersNeeded; + + /// Hourly pay rate. + final double hourlyRate; + + /// List of confirmed worker applications. + final List> confirmedApps; + + @override + List get props => [ + id, + title, + clientName, + status, + date, + startTime, + endTime, + location, + locationAddress, + filled, + workersNeeded, + hourlyRate, + confirmedApps, + ]; +} diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_get_started_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_get_started_page.dart index f673e78d..380efaad 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_get_started_page.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_get_started_page.dart @@ -97,6 +97,7 @@ class ClientGetStartedPage extends StatelessWidget { .get_started_page .sign_in_button, onPressed: () => Modular.to.pushClientSignIn(), + fullWidth: true, ), const SizedBox(height: UiConstants.space3), @@ -108,6 +109,7 @@ class ClientGetStartedPage extends StatelessWidget { .get_started_page .create_account_button, onPressed: () => Modular.to.pushClientSignUp(), + fullWidth: true, ), ], ), diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart index c1489ae6..9887a9cb 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart @@ -96,6 +96,7 @@ class _ClientSignInFormState extends State { UiButton.primary( text: widget.isLoading ? null : i18n.sign_in_button, onPressed: widget.isLoading ? null : _handleSubmit, + fullWidth: true, child: widget.isLoading ? const SizedBox( height: 24, diff --git a/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart index 60337e31..2569e7fd 100644 --- a/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart +++ b/apps/mobile/packages/features/client/client_main/lib/src/client_main_module.dart @@ -1,6 +1,7 @@ import 'package:client_home/client_home.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:view_orders/view_orders.dart'; import 'presentation/blocs/client_main_cubit.dart'; import 'presentation/pages/client_main_page.dart'; @@ -30,11 +31,7 @@ class ClientMainModule extends Module { child: (BuildContext context) => const PlaceholderPage(title: 'Billing'), ), - ChildRoute( - '/orders', - child: (BuildContext context) => - const PlaceholderPage(title: 'Orders'), - ), + ModuleRoute('/orders', module: ViewOrdersModule()), ChildRoute( '/reports', child: (BuildContext context) => diff --git a/apps/mobile/packages/features/client/client_main/pubspec.yaml b/apps/mobile/packages/features/client/client_main/pubspec.yaml index 48a037b6..c2ac9a6f 100644 --- a/apps/mobile/packages/features/client/client_main/pubspec.yaml +++ b/apps/mobile/packages/features/client/client_main/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: path: ../../../core_localization client_home: path: ../home + view_orders: + path: ../view_orders # Intentionally commenting these out as they might not exist yet # client_settings: # path: ../settings 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 ddb2ff8e..4ce8b483 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 @@ -8,7 +8,7 @@ import 'client_create_order_state.dart'; class ClientCreateOrderBloc extends Bloc { ClientCreateOrderBloc(this._getOrderTypesUseCase) - : super(const ClientCreateOrderInitial()) { + : super(const ClientCreateOrderInitial()) { on(_onTypesRequested); } final GetOrderTypesUseCase _getOrderTypesUseCase; diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart index c2db55cb..a4a0d193 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart @@ -8,16 +8,71 @@ 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()) { + : super(OneTimeOrderState.initial()) { + on(_onVendorsLoaded); + on(_onVendorChanged); on(_onDateChanged); on(_onLocationChanged); on(_onPositionAdded); on(_onPositionRemoved); on(_onPositionUpdated); on(_onSubmitted); + + // Initial load of mock vendors + add( + const OneTimeOrderVendorsLoaded([ + Vendor( + id: 'v1', + name: 'Elite Staffing', + rates: { + 'Server': 25.0, + 'Bartender': 30.0, + 'Cook': 28.0, + 'Busser': 18.0, + 'Host': 20.0, + 'Barista': 22.0, + 'Dishwasher': 17.0, + 'Event Staff': 19.0, + }, + ), + Vendor( + id: 'v2', + name: 'Premier Workforce', + rates: { + 'Server': 22.0, + 'Bartender': 28.0, + 'Cook': 25.0, + 'Busser': 16.0, + 'Host': 18.0, + 'Barista': 20.0, + 'Dishwasher': 15.0, + 'Event Staff': 18.0, + }, + ), + ]), + ); } final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; + void _onVendorsLoaded( + OneTimeOrderVendorsLoaded event, + Emitter emit, + ) { + emit( + state.copyWith( + vendors: event.vendors, + selectedVendor: event.vendors.isNotEmpty ? event.vendors.first : null, + ), + ); + } + + void _onVendorChanged( + OneTimeOrderVendorChanged event, + Emitter emit, + ) { + emit(state.copyWith(selectedVendor: event.vendor)); + } + void _onDateChanged( OneTimeOrderDateChanged event, Emitter emit, @@ -37,13 +92,14 @@ class OneTimeOrderBloc extends Bloc { Emitter emit, ) { final List newPositions = - List.from(state.positions) - ..add(const OneTimeOrderPosition( + List.from(state.positions)..add( + const OneTimeOrderPosition( role: '', count: 1, - startTime: '', - endTime: '', - )); + startTime: '09:00', + endTime: '17:00', + ), + ); emit(state.copyWith(positions: newPositions)); } @@ -79,14 +135,17 @@ class OneTimeOrderBloc extends Bloc { date: state.date, location: state.location, positions: state.positions, + // In a real app, we'd pass the vendorId here ); await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order)); emit(state.copyWith(status: OneTimeOrderStatus.success)); } catch (e) { - emit(state.copyWith( - status: OneTimeOrderStatus.failure, - errorMessage: e.toString(), - )); + 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 index 749bbb2e..ec9d4fcd 100644 --- 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 @@ -8,6 +8,22 @@ abstract class OneTimeOrderEvent extends Equatable { List get props => []; } +class OneTimeOrderVendorsLoaded extends OneTimeOrderEvent { + const OneTimeOrderVendorsLoaded(this.vendors); + final List vendors; + + @override + List get props => [vendors]; +} + +class OneTimeOrderVendorChanged extends OneTimeOrderEvent { + const OneTimeOrderVendorChanged(this.vendor); + final Vendor vendor; + + @override + List get props => [vendor]; +} + class OneTimeOrderDateChanged extends OneTimeOrderEvent { const OneTimeOrderDateChanged(this.date); final DateTime date; 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 index 2ef862f6..03aee2fa 100644 --- 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 @@ -10,6 +10,8 @@ class OneTimeOrderState extends Equatable { required this.positions, this.status = OneTimeOrderStatus.initial, this.errorMessage, + this.vendors = const [], + this.selectedVendor, }); factory OneTimeOrderState.initial() { @@ -17,13 +19,9 @@ class OneTimeOrderState extends Equatable { date: DateTime.now(), location: '', positions: const [ - OneTimeOrderPosition( - role: '', - count: 1, - startTime: '', - endTime: '', - ), + OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''), ], + vendors: const [], ); } final DateTime date; @@ -31,6 +29,8 @@ class OneTimeOrderState extends Equatable { final List positions; final OneTimeOrderStatus status; final String? errorMessage; + final List vendors; + final Vendor? selectedVendor; OneTimeOrderState copyWith({ DateTime? date, @@ -38,6 +38,8 @@ class OneTimeOrderState extends Equatable { List? positions, OneTimeOrderStatus? status, String? errorMessage, + List? vendors, + Vendor? selectedVendor, }) { return OneTimeOrderState( date: date ?? this.date, @@ -45,15 +47,19 @@ class OneTimeOrderState extends Equatable { positions: positions ?? this.positions, status: status ?? this.status, errorMessage: errorMessage ?? this.errorMessage, + vendors: vendors ?? this.vendors, + selectedVendor: selectedVendor ?? this.selectedVendor, ); } @override List get props => [ - date, - location, - positions, - status, - errorMessage, - ]; + date, + location, + positions, + status, + errorMessage, + vendors, + selectedVendor, + ]; } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart index 820baa04..f3b3b63b 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart @@ -7,15 +7,15 @@ 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"', - ], - ), - ) { + : 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); @@ -68,7 +68,8 @@ class RapidOrderBloc extends Bloc { try { await _createRapidOrderUseCase( - RapidOrderArguments(description: message)); + RapidOrderArguments(description: message), + ); emit(const RapidOrderSuccess()); } catch (e) { emit(RapidOrderFailure(e.toString())); 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 9660439f..641363e2 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 @@ -17,8 +17,9 @@ class ClientCreateOrderPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (BuildContext context) => Modular.get() - ..add(const ClientCreateOrderTypesRequested()), + create: (BuildContext context) => + Modular.get() + ..add(const ClientCreateOrderTypesRequested()), child: const CreateOrderView(), ); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart index bc007565..eb1775fb 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart @@ -65,59 +65,58 @@ class CreateOrderView extends StatelessWidget { ), ), Expanded( - child: - BlocBuilder( + child: BlocBuilder( builder: (BuildContext context, ClientCreateOrderState state) { - if (state is ClientCreateOrderLoadSuccess) { - return GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: UiConstants.space4, - crossAxisSpacing: UiConstants.space4, - childAspectRatio: 1, - ), - itemCount: state.orderTypes.length, - itemBuilder: (BuildContext context, int index) { - final OrderType type = state.orderTypes[index]; - final OrderTypeUiMetadata ui = - OrderTypeUiMetadata.fromId(id: type.id); + if (state is ClientCreateOrderLoadSuccess) { + return GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: UiConstants.space4, + crossAxisSpacing: UiConstants.space4, + childAspectRatio: 1, + ), + itemCount: state.orderTypes.length, + itemBuilder: (BuildContext context, int index) { + final OrderType type = state.orderTypes[index]; + final OrderTypeUiMetadata ui = + OrderTypeUiMetadata.fromId(id: type.id); - return OrderTypeCard( - icon: ui.icon, - title: _getTranslation(key: type.titleKey), - description: _getTranslation( - key: type.descriptionKey, - ), - backgroundColor: ui.backgroundColor, - borderColor: ui.borderColor, - iconBackgroundColor: ui.iconBackgroundColor, - iconColor: ui.iconColor, - textColor: ui.textColor, - descriptionColor: ui.descriptionColor, - onTap: () { - switch (type.id) { - case 'rapid': - Modular.to.pushRapidOrder(); - break; - case 'one-time': - Modular.to.pushOneTimeOrder(); - break; - case 'recurring': - Modular.to.pushRecurringOrder(); - break; - case 'permanent': - Modular.to.pushPermanentOrder(); - break; - } + return OrderTypeCard( + icon: ui.icon, + title: _getTranslation(key: type.titleKey), + description: _getTranslation( + key: type.descriptionKey, + ), + backgroundColor: ui.backgroundColor, + borderColor: ui.borderColor, + iconBackgroundColor: ui.iconBackgroundColor, + iconColor: ui.iconColor, + textColor: ui.textColor, + descriptionColor: ui.descriptionColor, + onTap: () { + switch (type.id) { + case 'rapid': + Modular.to.pushRapidOrder(); + break; + case 'one-time': + Modular.to.pushOneTimeOrder(); + break; + case 'recurring': + Modular.to.pushRecurringOrder(); + break; + case 'permanent': + Modular.to.pushPermanentOrder(); + break; + } + }, + ); }, ); - }, - ); - } - return const Center(child: CircularProgressIndicator()); - }, + } + return const Center(child: CircularProgressIndicator()); + }, ), ), ], diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart index 3dbf2a38..d39f6c8b 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart @@ -54,9 +54,7 @@ class OneTimeOrderHeader extends StatelessWidget { children: [ Text( title, - style: UiTypography.headline3m.copyWith( - color: UiColors.white, - ), + style: UiTypography.headline3m.copyWith(color: UiColors.white), ), Text( subtitle, diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart index 4b24cdfb..4af5d168 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart @@ -19,6 +19,7 @@ class OneTimeOrderPositionCard extends StatelessWidget { required this.startLabel, required this.endLabel, required this.lunchLabel, + this.vendor, super.key, }); @@ -55,6 +56,9 @@ class OneTimeOrderPositionCard extends StatelessWidget { /// Label for the lunch break. final String lunchLabel; + /// The current selected vendor to determine rates. + final Vendor? vendor; + @override Widget build(BuildContext context) { return Container( @@ -99,8 +103,10 @@ class OneTimeOrderPositionCard extends StatelessWidget { child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, - hint: - Text(roleLabel, style: UiTypography.body2r.textPlaceholder), + hint: Text( + roleLabel, + style: UiTypography.body2r.textPlaceholder, + ), value: position.role.isEmpty ? null : position.role, icon: const Icon( UiIcons.chevronDown, @@ -112,26 +118,26 @@ class OneTimeOrderPositionCard extends StatelessWidget { onUpdated(position.copyWith(role: val)); } }, - items: [ - 'Server', - 'Bartender', - 'Cook', - 'Busser', - 'Host', - 'Barista', - 'Dishwasher', - 'Event Staff' - ].map((String role) { - // Mock rates for UI matching - final int rate = _getMockRate(role); - return DropdownMenuItem( - value: role, - child: Text( - '$role - \$$rate/hr', - style: UiTypography.body2r.textPrimary, - ), - ); - }).toList(), + items: + { + ...(vendor?.rates.keys ?? []), + if (position.role.isNotEmpty && + !(vendor?.rates.keys.contains(position.role) ?? + false)) + position.role, + }.map((String role) { + final double? rate = vendor?.rates[role]; + final String label = rate == null + ? role + : '$role - \$${rate.toStringAsFixed(0)}/hr'; + return DropdownMenuItem( + value: role, + child: Text( + label, + style: UiTypography.body2r.textPrimary, + ), + ); + }).toList(), ), ), ), @@ -153,7 +159,8 @@ class OneTimeOrderPositionCard extends StatelessWidget { ); if (picked != null && context.mounted) { onUpdated( - position.copyWith(startTime: picked.format(context))); + position.copyWith(startTime: picked.format(context)), + ); } }, ), @@ -172,7 +179,8 @@ class OneTimeOrderPositionCard extends StatelessWidget { ); if (picked != null && context.mounted) { onUpdated( - position.copyWith(endTime: picked.format(context))); + position.copyWith(endTime: picked.format(context)), + ); } }, ), @@ -198,10 +206,13 @@ class OneTimeOrderPositionCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ GestureDetector( - onTap: () => onUpdated(position.copyWith( - count: (position.count > 1) - ? position.count - 1 - : 1)), + onTap: () { + if (position.count > 1) { + onUpdated( + position.copyWith(count: position.count - 1), + ); + } + }, child: const Icon(UiIcons.minus, size: 12), ), Text( @@ -209,8 +220,11 @@ class OneTimeOrderPositionCard extends StatelessWidget { style: UiTypography.body2b.textPrimary, ), GestureDetector( - onTap: () => onUpdated( - position.copyWith(count: position.count + 1)), + onTap: () { + onUpdated( + position.copyWith(count: position.count + 1), + ); + }, child: const Icon(UiIcons.add, size: 12), ), ], @@ -223,74 +237,12 @@ class OneTimeOrderPositionCard extends StatelessWidget { ), const SizedBox(height: UiConstants.space4), - // Optional Location Override - if (position.location == null) - GestureDetector( - onTap: () => onUpdated(position.copyWith(location: '')), - child: Row( - children: [ - const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary), - const SizedBox(width: UiConstants.space1), - Text( - t.client_create_order.one_time.different_location, - style: UiTypography.footnote1m.copyWith( - color: UiColors.primary, - ), - ), - ], - ), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const Icon(UiIcons.mapPin, - size: 14, color: UiColors.iconSecondary), - const SizedBox(width: UiConstants.space1), - Text( - t.client_create_order.one_time - .different_location_title, - style: UiTypography.footnote1m.textSecondary, - ), - ], - ), - GestureDetector( - onTap: () => onUpdated(position.copyWith(location: null)), - child: const Icon( - UiIcons.close, - size: 14, - color: UiColors.destructive, - ), - ), - ], - ), - const SizedBox(height: UiConstants.space2), - _PositionLocationInput( - value: position.location ?? '', - onChanged: (String val) => - onUpdated(position.copyWith(location: val)), - hintText: - t.client_create_order.one_time.different_location_hint, - ), - ], - ), - - const SizedBox(height: UiConstants.space3), - // Lunch Break - Text( - lunchLabel, - style: UiTypography.footnote2r.textSecondary, - ), + Text(lunchLabel, style: UiTypography.footnote2r.textSecondary), const SizedBox(height: UiConstants.space1), Container( - height: 44, padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + height: 44, decoration: BoxDecoration( borderRadius: UiConstants.radiusMd, border: Border.all(color: UiColors.border), @@ -309,43 +261,15 @@ class OneTimeOrderPositionCard extends StatelessWidget { onUpdated(position.copyWith(lunchBreak: val)); } }, - items: >[ - DropdownMenuItem( - value: 0, - child: Text(t.client_create_order.one_time.no_break, - style: UiTypography.body2r.textPrimary), - ), - DropdownMenuItem( - value: 10, + items: [0, 15, 30, 45, 60].map((int mins) { + return DropdownMenuItem( + value: mins, child: Text( - '10 ${t.client_create_order.one_time.paid_break}', - style: UiTypography.body2r.textPrimary), - ), - DropdownMenuItem( - value: 15, - child: Text( - '15 ${t.client_create_order.one_time.paid_break}', - style: UiTypography.body2r.textPrimary), - ), - DropdownMenuItem( - value: 30, - child: Text( - '30 ${t.client_create_order.one_time.unpaid_break}', - style: UiTypography.body2r.textPrimary), - ), - DropdownMenuItem( - value: 45, - child: Text( - '45 ${t.client_create_order.one_time.unpaid_break}', - style: UiTypography.body2r.textPrimary), - ), - DropdownMenuItem( - value: 60, - child: Text( - '60 ${t.client_create_order.one_time.unpaid_break}', - style: UiTypography.body2r.textPrimary), - ), - ], + mins == 0 ? 'No Break' : '$mins mins', + style: UiTypography.body2r.textPrimary, + ), + ); + }).toList(), ), ), ), @@ -360,83 +284,37 @@ class OneTimeOrderPositionCard extends StatelessWidget { required String value, required VoidCallback onTap, }) { - return UiTextField( - label: label, - controller: TextEditingController(text: value), - readOnly: true, - onTap: onTap, - hintText: '--:--', - ); - } - - int _getMockRate(String role) { - switch (role) { - case 'Server': - return 18; - case 'Bartender': - return 22; - case 'Cook': - return 20; - case 'Busser': - return 16; - case 'Host': - return 17; - case 'Barista': - return 16; - case 'Dishwasher': - return 15; - case 'Event Staff': - return 20; - default: - return 15; - } - } -} - -class _PositionLocationInput extends StatefulWidget { - const _PositionLocationInput({ - required this.value, - required this.hintText, - required this.onChanged, - }); - - final String value; - final String hintText; - final ValueChanged onChanged; - - @override - State<_PositionLocationInput> createState() => _PositionLocationInputState(); -} - -class _PositionLocationInputState extends State<_PositionLocationInput> { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.value); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(_PositionLocationInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != _controller.text) { - _controller.text = widget.value; - } - } - - @override - Widget build(BuildContext context) { - return UiTextField( - controller: _controller, - onChanged: widget.onChanged, - hintText: widget.hintText, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: UiTypography.footnote2r.textSecondary), + const SizedBox(height: UiConstants.space1), + GestureDetector( + onTap: onTap, + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + borderRadius: UiConstants.radiusSm, + border: Border.all(color: UiColors.border), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value.isEmpty ? '--:--' : value, + style: UiTypography.body2r.textPrimary, + ), + const Icon( + UiIcons.clock, + size: 14, + color: UiColors.iconSecondary, + ), + ], + ), + ), + ), + ], ); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart index 61adb94a..a7bf2b1a 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart @@ -27,14 +27,28 @@ class OneTimeOrderSectionHeader extends StatelessWidget { children: [ Text(title, style: UiTypography.headline4m.textPrimary), if (actionLabel != null && onAction != null) - UiButton.text( + TextButton( onPressed: onAction, - leadingIcon: UiIcons.add, - text: actionLabel!, - iconSize: 16, style: TextButton.styleFrom( - minimumSize: const Size(0, 24), - maximumSize: const Size(0, 24), + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(UiIcons.add, size: 16, color: Color(0xFF0032A0)), + const SizedBox(width: UiConstants.space2), + Text( + actionLabel!, + style: const TextStyle( + color: Color(0xFF0032A0), + fontSize: 14, + fontWeight: + FontWeight.w500, // Added to match typical button text + ), + ), + ], ), ), ], diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart index 404cbb56..19a27567 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart @@ -35,6 +35,47 @@ class OneTimeOrderView extends StatelessWidget { ); } + if (state.vendors.isEmpty && + state.status != OneTimeOrderStatus.loading) { + return Scaffold( + backgroundColor: UiColors.bgPrimary, + body: Column( + children: [ + OneTimeOrderHeader( + title: labels.title, + subtitle: labels.subtitle, + onBack: () => Modular.to.pop(), + ), + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + UiIcons.search, + size: 64, + color: UiColors.iconInactive, + ), + const SizedBox(height: UiConstants.space4), + Text( + 'No Vendors Available', + style: UiTypography.headline3m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), + Text( + 'There are no staffing vendors associated with your account.', + style: UiTypography.body2r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ), + ); + } + return Scaffold( backgroundColor: UiColors.bgPrimary, body: Column( @@ -58,8 +99,9 @@ class OneTimeOrderView extends StatelessWidget { ? labels.creating : labels.create_order, isLoading: state.status == OneTimeOrderStatus.loading, - onPressed: () => BlocProvider.of(context) - .add(const OneTimeOrderSubmitted()), + onPressed: () => BlocProvider.of( + context, + ).add(const OneTimeOrderSubmitted()), ), ], ), @@ -87,37 +129,78 @@ class _OneTimeOrderForm extends StatelessWidget { ), const SizedBox(height: UiConstants.space4), + // Vendor Selection + Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + height: 48, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: state.selectedVendor, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (Vendor? vendor) { + if (vendor != null) { + BlocProvider.of( + context, + ).add(OneTimeOrderVendorChanged(vendor)); + } + }, + items: state.vendors.map((Vendor vendor) { + return DropdownMenuItem( + value: vendor, + child: Text( + vendor.name, + style: UiTypography.body2m.textPrimary, + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space4), + OneTimeOrderDatePicker( label: labels.date_label, value: state.date, - onChanged: (DateTime date) => - BlocProvider.of(context) - .add(OneTimeOrderDateChanged(date)), + onChanged: (DateTime date) => BlocProvider.of( + context, + ).add(OneTimeOrderDateChanged(date)), ), const SizedBox(height: UiConstants.space4), OneTimeOrderLocationInput( label: labels.location_label, value: state.location, - onChanged: (String location) => - BlocProvider.of(context) - .add(OneTimeOrderLocationChanged(location)), + onChanged: (String location) => BlocProvider.of( + context, + ).add(OneTimeOrderLocationChanged(location)), ), const SizedBox(height: UiConstants.space6), OneTimeOrderSectionHeader( title: labels.positions_title, actionLabel: labels.add_position, - onAction: () => BlocProvider.of(context) - .add(const OneTimeOrderPositionAdded()), + onAction: () => BlocProvider.of( + context, + ).add(const OneTimeOrderPositionAdded()), ), const SizedBox(height: UiConstants.space3), // Positions List - ...state.positions - .asMap() - .entries - .map((MapEntry entry) { + ...state.positions.asMap().entries.map(( + MapEntry entry, + ) { final int index = entry.key; final OneTimeOrderPosition position = entry.value; return Padding( @@ -132,14 +215,16 @@ class _OneTimeOrderForm extends StatelessWidget { startLabel: labels.start_label, endLabel: labels.end_label, lunchLabel: labels.lunch_break_label, + vendor: state.selectedVendor, onUpdated: (OneTimeOrderPosition updated) { - BlocProvider.of(context).add( - OneTimeOrderPositionUpdated(index, updated), - ); + BlocProvider.of( + context, + ).add(OneTimeOrderPositionUpdated(index, updated)); }, onRemoved: () { - BlocProvider.of(context) - .add(OneTimeOrderPositionRemoved(index)); + BlocProvider.of( + context, + ).add(OneTimeOrderPositionRemoved(index)); }, ), ); diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart index 9a6a4535..f9c92f43 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart @@ -73,16 +73,14 @@ class OrderTypeCard extends StatelessWidget { ), child: Icon(icon, color: iconColor, size: 24), ), - Text( - title, - style: UiTypography.body2b.copyWith(color: textColor), - ), + Text(title, style: UiTypography.body2b.copyWith(color: textColor)), const SizedBox(height: UiConstants.space1), Expanded( child: Text( description, - style: - UiTypography.footnote1r.copyWith(color: descriptionColor), + 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 index c2ce1723..7ffac143 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart @@ -47,10 +47,7 @@ class RapidOrderExampleCard extends StatelessWidget { text: TextSpan( style: UiTypography.body2r.textPrimary, children: [ - TextSpan( - text: label, - style: UiTypography.body2b.textPrimary, - ), + 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 index 2eec2d55..bcb4680e 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart @@ -74,11 +74,7 @@ class RapidOrderHeader extends StatelessWidget { children: [ Row( children: [ - const Icon( - UiIcons.zap, - color: UiColors.accent, - size: 18, - ), + const Icon(UiIcons.zap, color: UiColors.accent, size: 18), const SizedBox(width: UiConstants.space2), Text( title, diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart index e99b1bb4..1ad01b09 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart @@ -42,8 +42,9 @@ class RapidOrderSuccessView extends StatelessWidget { child: SafeArea( child: Center( child: Container( - margin: - const EdgeInsets.symmetric(horizontal: UiConstants.space10), + margin: const EdgeInsets.symmetric( + horizontal: UiConstants.space10, + ), padding: const EdgeInsets.all(UiConstants.space8), decoration: BoxDecoration( color: UiColors.white, @@ -75,10 +76,7 @@ class RapidOrderSuccessView extends StatelessWidget { ), ), const SizedBox(height: UiConstants.space6), - Text( - title, - style: UiTypography.headline1m.textPrimary, - ), + Text(title, style: UiTypography.headline1m.textPrimary), const SizedBox(height: UiConstants.space3), Text( message, diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart index fe03182d..1f758d89 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart @@ -153,27 +153,27 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { // Examples if (initialState != null) - ...initialState.examples - .asMap() - .entries - .map((MapEntry entry) { + ...initialState.examples.asMap().entries.map(( + MapEntry entry, + ) { final int index = entry.key; final String example = entry.value; final bool isHighlighted = index == 0; return Padding( padding: const EdgeInsets.only( - bottom: UiConstants.space2), + bottom: UiConstants.space2, + ), child: RapidOrderExampleCard( example: example, isHighlighted: isHighlighted, label: labels.example, onTap: () => BlocProvider.of( - context) - .add( - RapidOrderExampleSelected(example), - ), + context, + ).add( + RapidOrderExampleSelected(example), + ), ), ); }), @@ -184,9 +184,9 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { controller: _messageController, maxLines: 4, onChanged: (String value) { - BlocProvider.of(context).add( - RapidOrderMessageChanged(value), - ); + BlocProvider.of( + context, + ).add(RapidOrderMessageChanged(value)); }, hintText: labels.hint, ), @@ -197,7 +197,8 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { labels: labels, isSubmitting: isSubmitting, isListening: initialState?.isListening ?? false, - isMessageEmpty: initialState != null && + isMessageEmpty: + initialState != null && initialState.message.trim().isEmpty, ), ], @@ -242,11 +243,7 @@ class _AnimatedZapIcon extends StatelessWidget { ), ], ), - child: const Icon( - UiIcons.zap, - color: UiColors.white, - size: 32, - ), + child: const Icon(UiIcons.zap, color: UiColors.white, size: 32), ); } } @@ -271,9 +268,9 @@ class _RapidOrderActions extends StatelessWidget { child: UiButton.secondary( text: isListening ? labels.listening : labels.speak, leadingIcon: UiIcons.bell, // Placeholder for mic - onPressed: () => BlocProvider.of(context).add( - const RapidOrderVoiceToggled(), - ), + onPressed: () => BlocProvider.of( + context, + ).add(const RapidOrderVoiceToggled()), style: OutlinedButton.styleFrom( backgroundColor: isListening ? UiColors.destructive.withValues(alpha: 0.05) @@ -291,9 +288,9 @@ class _RapidOrderActions extends StatelessWidget { trailingIcon: UiIcons.arrowRight, onPressed: isSubmitting || isMessageEmpty ? null - : () => BlocProvider.of(context).add( - const RapidOrderSubmitted(), - ), + : () => BlocProvider.of( + context, + ).add(const RapidOrderSubmitted()), ), ), ], diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart new file mode 100644 index 00000000..cdc54d00 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart @@ -0,0 +1,17 @@ +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/repositories/i_view_orders_repository.dart'; + +/// Implementation of [IViewOrdersRepository] providing data from [OrderRepositoryMock]. +class ViewOrdersRepositoryImpl implements IViewOrdersRepository { + final OrderRepositoryMock _orderRepositoryMock; + + /// Creates a [ViewOrdersRepositoryImpl] with the given [OrderRepositoryMock]. + ViewOrdersRepositoryImpl({required OrderRepositoryMock orderRepositoryMock}) + : _orderRepositoryMock = orderRepositoryMock; + + @override + Future> getOrders() { + return _orderRepositoryMock.getOrders(); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/domain/repositories/i_view_orders_repository.dart b/apps/mobile/packages/features/client/view_orders/lib/src/domain/repositories/i_view_orders_repository.dart new file mode 100644 index 00000000..d6b129ed --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/domain/repositories/i_view_orders_repository.dart @@ -0,0 +1,7 @@ +import 'package:krow_domain/krow_domain.dart'; + +/// Repository interface for fetching and managing client orders. +abstract class IViewOrdersRepository { + /// Fetches a list of [OrderItem] for the client. + Future> getOrders(); +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/domain/usecases/get_orders_use_case.dart b/apps/mobile/packages/features/client/view_orders/lib/src/domain/usecases/get_orders_use_case.dart new file mode 100644 index 00000000..3f0018e2 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/domain/usecases/get_orders_use_case.dart @@ -0,0 +1,19 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/i_view_orders_repository.dart'; + +/// Use case for retrieving the list of client orders. +/// +/// This use case encapsulates the business rule of fetching orders +/// and delegates the data retrieval to the [IViewOrdersRepository]. +class GetOrdersUseCase implements NoInputUseCase> { + final IViewOrdersRepository _repository; + + /// Creates a [GetOrdersUseCase] with the required [IViewOrdersRepository]. + GetOrdersUseCase(this._repository); + + @override + Future> call() { + return _repository.getOrders(); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart new file mode 100644 index 00000000..72ecfb6c --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_cubit.dart @@ -0,0 +1,156 @@ +import 'package:intl/intl.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/usecases/get_orders_use_case.dart'; +import 'view_orders_state.dart'; + +/// Cubit for managing the state of the View Orders feature. +/// +/// This Cubit handles loading orders, date selection, and tab filtering. +class ViewOrdersCubit extends Cubit { + ViewOrdersCubit({required GetOrdersUseCase getOrdersUseCase}) + : _getOrdersUseCase = getOrdersUseCase, + super(ViewOrdersState(selectedDate: DateTime.now())) { + _init(); + } + + final GetOrdersUseCase _getOrdersUseCase; + + void _init() { + updateWeekOffset(0); // Initialize calendar days + loadOrders(); + } + + /// Loads the list of orders using the [GetOrdersUseCase]. + Future loadOrders() async { + emit(state.copyWith(status: ViewOrdersStatus.loading)); + try { + final List orders = await _getOrdersUseCase(); + emit(state.copyWith(status: ViewOrdersStatus.success, orders: orders)); + _updateDerivedState(); + } catch (_) { + emit(state.copyWith(status: ViewOrdersStatus.failure)); + } + } + + void selectDate(DateTime date) { + emit(state.copyWith(selectedDate: date)); + _updateDerivedState(); + } + + void selectFilterTab(String tabId) { + emit(state.copyWith(filterTab: tabId)); + _updateDerivedState(); + } + + void updateWeekOffset(int offset) { + final int newWeekOffset = state.weekOffset + offset; + final List calendarDays = _calculateCalendarDays(newWeekOffset); + emit(state.copyWith(weekOffset: newWeekOffset, calendarDays: calendarDays)); + _updateDerivedState(); + } + + void _updateDerivedState() { + final List filteredOrders = _calculateFilteredOrders(state); + final int activeCount = _calculateCategoryCount('active'); + final int completedCount = _calculateCategoryCount('completed'); + final int upNextCount = _calculateUpNextCount(); + + emit( + state.copyWith( + filteredOrders: filteredOrders, + activeCount: activeCount, + completedCount: completedCount, + upNextCount: upNextCount, + ), + ); + } + + List _calculateCalendarDays(int weekOffset) { + final DateTime now = DateTime.now(); + final int jsDay = now.weekday == 7 ? 0 : now.weekday; + final int daysSinceFriday = (jsDay + 2) % 7; + + final DateTime startDate = DateTime(now.year, now.month, now.day) + .subtract(Duration(days: daysSinceFriday)) + .add(Duration(days: weekOffset * 7)); + + return List.generate( + 7, + (int index) => startDate.add(Duration(days: index)), + ); + } + + List _calculateFilteredOrders(ViewOrdersState state) { + if (state.selectedDate == null) return []; + + final String selectedDateStr = DateFormat( + 'yyyy-MM-dd', + ).format(state.selectedDate!); + + // Filter by date + final List ordersOnDate = state.orders + .where((OrderItem s) => s.date == selectedDateStr) + .toList(); + + // Sort by start time + ordersOnDate.sort( + (OrderItem a, OrderItem b) => a.startTime.compareTo(b.startTime), + ); + + if (state.filterTab == 'all') { + return ordersOnDate + .where( + (OrderItem s) => + ['open', 'filled', 'confirmed'].contains(s.status), + ) + .toList(); + } else if (state.filterTab == 'active') { + return ordersOnDate + .where((OrderItem s) => s.status == 'in_progress') + .toList(); + } else if (state.filterTab == 'completed') { + return ordersOnDate + .where((OrderItem s) => s.status == 'completed') + .toList(); + } + return []; + } + + int _calculateCategoryCount(String category) { + if (state.selectedDate == null) return 0; + final String selectedDateStr = DateFormat( + 'yyyy-MM-dd', + ).format(state.selectedDate!); + final List ordersOnDate = state.orders + .where((OrderItem s) => s.date == selectedDateStr) + .toList(); + + if (category == 'active') { + return ordersOnDate + .where((OrderItem s) => s.status == 'in_progress') + .length; + } else if (category == 'completed') { + return ordersOnDate + .where((OrderItem s) => s.status == 'completed') + .length; + } + return 0; + } + + int _calculateUpNextCount() { + if (state.selectedDate == null) return 0; + final String selectedDateStr = DateFormat( + 'yyyy-MM-dd', + ).format(state.selectedDate!); + final List ordersOnDate = state.orders + .where((OrderItem s) => s.date == selectedDateStr) + .toList(); + return ordersOnDate + .where( + (OrderItem s) => + ['open', 'filled', 'confirmed'].contains(s.status), + ) + .length; + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_state.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_state.dart new file mode 100644 index 00000000..af67fa19 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/blocs/view_orders_state.dart @@ -0,0 +1,70 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +enum ViewOrdersStatus { initial, loading, success, failure } + +class ViewOrdersState extends Equatable { + const ViewOrdersState({ + this.status = ViewOrdersStatus.initial, + this.orders = const [], + this.filteredOrders = const [], + this.calendarDays = const [], + this.selectedDate, + this.filterTab = 'all', + this.weekOffset = 0, + this.activeCount = 0, + this.completedCount = 0, + this.upNextCount = 0, + }); + + final ViewOrdersStatus status; + final List orders; + final List filteredOrders; + final List calendarDays; + final DateTime? selectedDate; + final String filterTab; + final int weekOffset; + final int activeCount; + final int completedCount; + final int upNextCount; + + ViewOrdersState copyWith({ + ViewOrdersStatus? status, + List? orders, + List? filteredOrders, + List? calendarDays, + DateTime? selectedDate, + String? filterTab, + int? weekOffset, + int? activeCount, + int? completedCount, + int? upNextCount, + }) { + return ViewOrdersState( + status: status ?? this.status, + orders: orders ?? this.orders, + filteredOrders: filteredOrders ?? this.filteredOrders, + calendarDays: calendarDays ?? this.calendarDays, + selectedDate: selectedDate ?? this.selectedDate, + filterTab: filterTab ?? this.filterTab, + weekOffset: weekOffset ?? this.weekOffset, + activeCount: activeCount ?? this.activeCount, + completedCount: completedCount ?? this.completedCount, + upNextCount: upNextCount ?? this.upNextCount, + ); + } + + @override + List get props => [ + status, + orders, + filteredOrders, + calendarDays, + selectedDate, + filterTab, + weekOffset, + activeCount, + completedCount, + upNextCount, + ]; +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart new file mode 100644 index 00000000..7160bb59 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/navigation/view_orders_navigator.dart @@ -0,0 +1,14 @@ +import 'package:flutter_modular/flutter_modular.dart'; + +/// Extension to provide typed navigation for the View Orders feature. +extension ViewOrdersNavigator on IModularNavigator { + /// Navigates to the Create Order feature. + void navigateToCreateOrder() { + pushNamed('/client/create-order/'); + } + + /// Navigates to the Order Details (placeholder for now). + void navigateToOrderDetails(String orderId) { + // pushNamed('/view-orders/$orderId'); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart new file mode 100644 index 00000000..c47b8518 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/pages/view_orders_page.dart @@ -0,0 +1,472 @@ +import 'dart:ui'; +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:core_localization/core_localization.dart'; +import '../blocs/view_orders_cubit.dart'; +import '../blocs/view_orders_state.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../widgets/view_order_card.dart'; +import '../navigation/view_orders_navigator.dart'; + +/// The main page for viewing client orders. +/// +/// This page follows the KROW Clean Architecture by: +/// - Being a [StatelessWidget]. +/// - Using [ViewOrdersCubit] for state management. +/// - Adhering to the project's Design System. +class ViewOrdersPage extends StatelessWidget { + /// Creates a [ViewOrdersPage]. + const ViewOrdersPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => Modular.get(), + child: const ViewOrdersView(), + ); + } +} + +/// The internal view implementation for [ViewOrdersPage]. +class ViewOrdersView extends StatelessWidget { + /// Creates a [ViewOrdersView]. + const ViewOrdersView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (BuildContext context, ViewOrdersState state) { + final List calendarDays = state.calendarDays; + final List filteredOrders = state.filteredOrders; + + // Header Colors logic from prototype + String sectionTitle = ''; + Color dotColor = UiColors.transparent; + + if (state.filterTab == 'all') { + sectionTitle = t.client_view_orders.tabs.up_next; + dotColor = UiColors.primary; + } else if (state.filterTab == 'active') { + sectionTitle = t.client_view_orders.tabs.active; + dotColor = UiColors.textWarning; + } else if (state.filterTab == 'completed') { + sectionTitle = t.client_view_orders.tabs.completed; + dotColor = + UiColors.primary; // Reverting to primary blue for consistency + } + + return Scaffold( + backgroundColor: UiColors.white, + body: Stack( + children: [ + // Background Gradient + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [UiColors.bgSecondary, UiColors.white], + stops: [0.0, 0.3], + ), + ), + ), + + SafeArea( + child: Column( + children: [ + // Header + Filter + Calendar (Sticky behavior) + _buildHeader( + context: context, + state: state, + calendarDays: calendarDays, + ), + + // Content List + Expanded( + child: filteredOrders.isEmpty + ? _buildEmptyState(context: context, state: state) + : ListView( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space4, + UiConstants.space5, + 100, + ), + children: [ + if (filteredOrders.isNotEmpty) + Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: dotColor, + shape: BoxShape.circle, + ), + ), + const SizedBox( + width: UiConstants.space2, + ), + Text( + sectionTitle.toUpperCase(), + style: UiTypography.titleUppercase2m + .copyWith( + color: UiColors.textPrimary, + ), + ), + const SizedBox( + width: UiConstants.space1, + ), + Text( + '(${filteredOrders.length})', + style: UiTypography.footnote1r + .copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ), + ...filteredOrders.map( + (OrderItem order) => Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space3, + ), + child: ViewOrderCard(order: order), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + /// Builds the sticky header section. + Widget _buildHeader({ + required BuildContext context, + required ViewOrdersState state, + required List calendarDays, + }) { + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: const BoxDecoration( + color: Color(0xCCFFFFFF), // White with 0.8 alpha + border: Border( + bottom: BorderSide(color: UiColors.separatorSecondary), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Top Bar + Padding( + padding: const EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + UiConstants.space3, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + t.client_view_orders.title, + style: UiTypography.headline3m.copyWith( + color: UiColors.textPrimary, + fontWeight: FontWeight.bold, + ), + ), + UiButton.primary( + text: t.client_view_orders.post_button, + leadingIcon: UiIcons.add, + onPressed: () => Modular.to.navigateToCreateOrder(), + size: UiButtonSize.small, + style: ElevatedButton.styleFrom( + minimumSize: const Size(0, 48), + maximumSize: const Size(0, 48), + ), + ), + ], + ), + ), + + // Filter Tabs + Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildFilterTab( + context, + label: t.client_view_orders.tabs.up_next, + isSelected: state.filterTab == 'all', + tabId: 'all', + ), + const SizedBox(width: UiConstants.space6), + _buildFilterTab( + context, + label: t.client_view_orders.tabs.active, + isSelected: state.filterTab == 'active', + tabId: 'active', + count: state.activeCount + state.upNextCount, + ), + const SizedBox(width: UiConstants.space6), + _buildFilterTab( + context, + label: t.client_view_orders.tabs.completed, + isSelected: state.filterTab == 'completed', + tabId: 'completed', + count: state.completedCount, + ), + ], + ), + ), + + // Calendar Header controls + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + vertical: UiConstants.space2, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon( + UiIcons.chevronLeft, + size: 20, + color: UiColors.iconSecondary, + ), + onPressed: () => BlocProvider.of( + context, + ).updateWeekOffset(-1), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + splashRadius: 20, + ), + Text( + DateFormat('MMMM yyyy').format(calendarDays.first), + style: UiTypography.body2m.copyWith( + color: UiColors.textSecondary, + ), + ), + IconButton( + icon: const Icon( + UiIcons.chevronRight, + size: 20, + color: UiColors.iconSecondary, + ), + onPressed: () => BlocProvider.of( + context, + ).updateWeekOffset(1), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + splashRadius: 20, + ), + ], + ), + ), + + // Calendar Grid + SizedBox( + height: 72, + child: ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space5, + ), + scrollDirection: Axis.horizontal, + itemCount: 7, + separatorBuilder: (BuildContext context, int index) => + const SizedBox(width: UiConstants.space2), + itemBuilder: (BuildContext context, int index) { + final DateTime date = calendarDays[index]; + final bool isSelected = + state.selectedDate != null && + date.year == state.selectedDate!.year && + date.month == state.selectedDate!.month && + date.day == state.selectedDate!.day; + + // Check if this date has any shifts + final String dateStr = DateFormat( + 'yyyy-MM-dd', + ).format(date); + final bool hasShifts = state.orders.any( + (OrderItem s) => s.date == dateStr, + ); + + return GestureDetector( + onTap: () => BlocProvider.of( + context, + ).selectDate(date), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 48, + decoration: BoxDecoration( + color: isSelected ? UiColors.primary : UiColors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? UiColors.primary + : UiColors.separatorPrimary, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: UiColors.primary.withValues( + alpha: 0.25, + ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ] + : null, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + DateFormat('dd').format(date), + style: UiTypography.title2b.copyWith( + fontSize: 18, + color: isSelected + ? UiColors.white + : UiColors.textPrimary, + ), + ), + Text( + DateFormat('E').format(date), + style: UiTypography.footnote2m.copyWith( + color: isSelected + ? UiColors.white.withValues(alpha: 0.8) + : UiColors.textSecondary, + ), + ), + if (hasShifts) ...[ + const SizedBox(height: UiConstants.space1), + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: isSelected + ? UiColors.white + : UiColors.primary, + shape: BoxShape.circle, + ), + ), + ], + ], + ), + ), + ); + }, + ), + ), + const SizedBox(height: UiConstants.space4), + ], + ), + ), + ), + ); + } + + /// Builds a single filter tab. + Widget _buildFilterTab( + BuildContext context, { + required String label, + required bool isSelected, + required String tabId, + int? count, + }) { + String text = label; + if (count != null) { + text = '$label ($count)'; + } + + return GestureDetector( + onTap: () => + BlocProvider.of(context).selectFilterTab(tabId), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space2), + child: Text( + text, + style: UiTypography.body2m.copyWith( + color: isSelected ? UiColors.primary : UiColors.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: 2, + width: isSelected ? 40 : 0, + decoration: BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + if (!isSelected) const SizedBox(height: 2), + ], + ), + ); + } + + /// Builds the empty state view. + Widget _buildEmptyState({ + required BuildContext context, + required ViewOrdersState state, + }) { + final String dateStr = state.selectedDate != null + ? _formatDateHeader(state.selectedDate!) + : 'this date'; + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(UiIcons.calendar, size: 48, color: UiColors.iconInactive), + const SizedBox(height: UiConstants.space3), + Text( + t.client_view_orders.no_orders(date: dateStr), + style: UiTypography.body2r.copyWith(color: UiColors.textSecondary), + ), + const SizedBox(height: UiConstants.space4), + UiButton.primary( + text: t.client_view_orders.post_order, + leadingIcon: UiIcons.add, + onPressed: () => Modular.to.navigateToCreateOrder(), + ), + ], + ), + ); + } + + static String _formatDateHeader(DateTime date) { + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + final DateTime tomorrow = today.add(const Duration(days: 1)); + final DateTime checkDate = DateTime(date.year, date.month, date.day); + + if (checkDate == today) return 'Today'; + if (checkDate == tomorrow) return 'Tomorrow'; + return DateFormat('EEE, MMM d').format(date); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart new file mode 100644 index 00000000..8753ecd0 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart @@ -0,0 +1,1554 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A rich card displaying details of a client order/shift. +/// +/// This widget complies with the KROW Design System by using +/// tokens from `package:design_system`. +class ViewOrderCard extends StatefulWidget { + /// Creates a [ViewOrderCard] for the given [order]. + const ViewOrderCard({required this.order, super.key}); + + /// The order item to display. + final OrderItem order; + + @override + State createState() => _ViewOrderCardState(); +} + +class _ViewOrderCardState extends State { + bool _expanded = true; + + void _openEditSheet({required OrderItem order}) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) => _OrderEditSheet(order: order), + ); + } + + /// Returns the semantic color for the given status. + Color _getStatusColor({required String status}) { + switch (status) { + case 'open': + return UiColors.primary; + case 'filled': + case 'confirmed': + return UiColors.textSuccess; + case 'in_progress': + return UiColors.textWarning; + case 'completed': + return UiColors.primary; + case 'cancelled': + return UiColors.destructive; + default: + return UiColors.textSecondary; + } + } + + /// Returns the localized label for the given status. + String _getStatusLabel({required String status}) { + switch (status) { + case 'open': + return t.client_view_orders.card.open; + case 'filled': + return t.client_view_orders.card.filled; + case 'confirmed': + return t.client_view_orders.card.confirmed; + case 'in_progress': + return t.client_view_orders.card.in_progress; + case 'completed': + return t.client_view_orders.card.completed; + case 'cancelled': + return t.client_view_orders.card.cancelled; + default: + return status.toUpperCase(); + } + } + + /// Formats the date string for display. + String _formatDate({required String dateStr}) { + try { + final DateTime date = DateTime.parse(dateStr); + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + final DateTime tomorrow = today.add(const Duration(days: 1)); + final DateTime checkDate = DateTime(date.year, date.month, date.day); + + if (checkDate == today) return 'Today'; + if (checkDate == tomorrow) return 'Tomorrow'; + return DateFormat('EEE, MMM d').format(date); + } catch (_) { + return dateStr; + } + } + + /// Formats the time string for display. + String _formatTime({required String timeStr}) { + if (timeStr.isEmpty) return ''; + try { + final List parts = timeStr.split(':'); + int hour = int.parse(parts[0]); + final int minute = int.parse(parts[1]); + final String ampm = hour >= 12 ? 'PM' : 'AM'; + hour = hour % 12; + if (hour == 0) hour = 12; + return '$hour:${minute.toString().padLeft(2, '0')} $ampm'; + } catch (_) { + return timeStr; + } + } + + @override + Widget build(BuildContext context) { + final OrderItem order = widget.order; + final Color statusColor = _getStatusColor(status: order.status); + final String statusLabel = _getStatusLabel(status: order.status); + final int coveragePercent = order.workersNeeded > 0 + ? ((order.filled / order.workersNeeded) * 100).round() + : 0; + + // Simulation of cost/hours calculation + const double hours = 8.0; + final double cost = + order.hourlyRate * + hours * + (order.filled > 0 ? order.filled : order.workersNeeded); + + return Container( + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Status Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + statusLabel.toUpperCase(), + style: UiTypography.footnote2b.copyWith( + color: statusColor, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + const SizedBox(height: UiConstants.space3), + // Title + Text( + order.title, + style: UiTypography.headline4m.textPrimary, + ), + const SizedBox(height: UiConstants.space1), + // Client & Date + Row( + children: [ + Text( + order.clientName, + style: UiTypography.body3r.textSecondary, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6, + ), + child: Text( + '•', + style: UiTypography.body3r.textInactive, + ), + ), + Text( + _formatDate(dateStr: order.date), + style: UiTypography.body3m.textSecondary, + ), + ], + ), + const SizedBox(height: UiConstants.space2), + // Address + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 14, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + order.locationAddress, + style: UiTypography.footnote2r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(width: UiConstants.space3), + // Actions + Row( + children: [ + _buildHeaderIconButton( + icon: UiIcons.edit, + color: UiColors.primary, + bgColor: UiColors.primary.withValues(alpha: 0.08), + onTap: () => _openEditSheet(order: order), + ), + const SizedBox(width: UiConstants.space2), + _buildHeaderIconButton( + icon: _expanded + ? UiIcons.chevronUp + : UiIcons.chevronDown, + color: UiColors.iconSecondary, + bgColor: UiColors.bgSecondary, + onTap: () => setState(() => _expanded = !_expanded), + ), + ], + ), + ], + ), + + const SizedBox(height: UiConstants.space4), + const Divider(height: 1, color: UiColors.border), + const SizedBox(height: UiConstants.space4), + + // Stats Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildStatItem( + icon: UiIcons.dollar, + value: '\$${cost.round()}', + label: 'Total', + ), + _buildStatDivider(), + _buildStatItem( + icon: UiIcons.clock, + value: hours.toStringAsFixed(1), + label: 'Hrs', + ), + _buildStatDivider(), + _buildStatItem( + icon: UiIcons.users, + value: + '${order.filled > 0 ? order.filled : order.workersNeeded}', + label: 'Workers', + ), + ], + ), + + const SizedBox(height: UiConstants.space5), + + // Times Section + Row( + children: [ + Expanded( + child: _buildTimeDisplay( + label: 'Clock In', + time: _formatTime(timeStr: order.startTime), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: _buildTimeDisplay( + label: 'Clock Out', + time: _formatTime(timeStr: order.endTime), + ), + ), + ], + ), + + const SizedBox(height: UiConstants.space4), + + // Coverage Section + if (order.status != 'completed') ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon( + UiIcons.success, + size: 16, + color: UiColors.textSuccess, + ), + const SizedBox(width: 8), + Text( + '${order.filled}/${order.workersNeeded} Workers Filled', + style: UiTypography.body2m.textPrimary, + ), + ], + ), + Text( + '$coveragePercent%', + style: UiTypography.body2b.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular(100), + child: LinearProgressIndicator( + value: coveragePercent / 100, + backgroundColor: UiColors.bgSecondary, + valueColor: const AlwaysStoppedAnimation( + UiColors.primary, + ), + minHeight: 8, + ), + ), + + // Avatar Stack Preview (if not expanded) + if (!_expanded && order.confirmedApps.isNotEmpty) ...[ + const SizedBox(height: UiConstants.space4), + Row( + children: [ + _buildAvatarStack(order.confirmedApps), + if (order.confirmedApps.length > 3) + Padding( + padding: const EdgeInsets.only(left: 12), + child: Text( + '+${order.confirmedApps.length - 3} more', + style: UiTypography.footnote2r.textSecondary, + ), + ), + ], + ), + ], + ], + ], + ), + ), + + // Assigned Workers (Expanded section) + if (_expanded && order.confirmedApps.isNotEmpty) ...[ + Container( + decoration: const BoxDecoration( + color: UiColors.bgSecondary, + border: Border(top: BorderSide(color: UiColors.border)), + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12), + ), + ), + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'CONFIRMED WORKERS', + style: UiTypography.footnote2b.textSecondary, + ), + GestureDetector( + onTap: () {}, + child: Text( + 'Message All', + style: UiTypography.footnote2b.copyWith( + color: UiColors.primary, + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space4), + ...order.confirmedApps + .take(5) + .map((Map app) => _buildWorkerRow(app)), + if (order.confirmedApps.length > 5) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Center( + child: TextButton( + onPressed: () {}, + child: Text( + 'Show ${order.confirmedApps.length - 5} more workers', + style: UiTypography.body2m.copyWith( + color: UiColors.primary, + ), + ), + ), + ), + ), + ], + ), + ), + ], + ], + ), + ); + } + + Widget _buildStatDivider() { + return Container(width: 1, height: 24, color: UiColors.border); + } + + Widget _buildTimeDisplay({required String label, required String time}) { + return Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusMd, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label.toUpperCase(), + style: UiTypography.titleUppercase4m.textSecondary, + ), + const SizedBox(height: 4), + Text(time, style: UiTypography.body1b.textPrimary), + ], + ), + ); + } + + /// Builds a stacked avatar UI for a list of applications. + Widget _buildAvatarStack(List> apps) { + const double size = 32.0; + const double overlap = 22.0; + final int count = apps.length > 3 ? 3 : apps.length; + + return SizedBox( + height: size, + width: size + (count - 1) * overlap, + child: Stack( + children: [ + for (int i = 0; i < count; i++) + Positioned( + left: i * overlap, + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: UiColors.white, width: 2), + color: UiColors.primary.withValues(alpha: 0.1), + ), + child: Center( + child: Text( + (apps[i]['worker_name'] as String)[0], + style: UiTypography.footnote2b.copyWith( + color: UiColors.primary, + ), + ), + ), + ), + ), + ], + ), + ); + } + + /// Builds a detailed row for a worker. + Widget _buildWorkerRow(Map app) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: UiColors.primary.withValues(alpha: 0.1), + child: Text( + (app['worker_name'] as String)[0], + style: UiTypography.body1b.copyWith(color: UiColors.primary), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + app['worker_name'] as String, + style: UiTypography.body2m.textPrimary, + ), + const SizedBox(height: 2), + Row( + children: [ + const Icon(UiIcons.star, size: 10, color: UiColors.accent), + const SizedBox(width: 2), + Text('4.8', style: UiTypography.footnote2r.textSecondary), + if (app['check_in_time'] != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), + decoration: BoxDecoration( + color: UiColors.textSuccess.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Checked In', + style: UiTypography.titleUppercase4m.copyWith( + color: UiColors.textSuccess, + ), + ), + ), + ], + ], + ), + ], + ), + ), + _buildActionIconButton(icon: UiIcons.phone, onTap: () {}), + const SizedBox(width: 8), + _buildActionIconButton(icon: UiIcons.messageCircle, onTap: () {}), + ], + ), + ); + } + + /// Specialized action button for worker rows. + Widget _buildActionIconButton({ + required IconData icon, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 16, color: UiColors.primary), + ), + ); + } + + /// Builds a small icon button used in row headers. + Widget _buildHeaderIconButton({ + required IconData icon, + required Color color, + required Color bgColor, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: bgColor, + borderRadius: UiConstants.radiusSm, + ), + child: Icon(icon, size: 16, color: color), + ), + ); + } + + /// Builds a single stat item (e.g., Cost, Hours, Workers). + Widget _buildStatItem({ + required IconData icon, + required String value, + required String label, + }) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 14, color: UiColors.iconSecondary), + const SizedBox(width: 6), + Text(value, style: UiTypography.body1b.textPrimary), + ], + ), + const SizedBox(height: 2), + Text( + label.toUpperCase(), + style: UiTypography.titleUppercase4m.textInactive, + ), + ], + ); + } +} + +/// A sophisticated bottom sheet for editing an existing order, +/// following the Unified Order Flow prototype and matching OneTimeOrderView. +class _OrderEditSheet extends StatefulWidget { + const _OrderEditSheet({required this.order}); + + final OrderItem order; + + @override + State<_OrderEditSheet> createState() => _OrderEditSheetState(); +} + +class _OrderEditSheetState extends State<_OrderEditSheet> { + bool _showReview = false; + bool _isLoading = false; + + late TextEditingController _dateController; + late TextEditingController _globalLocationController; + + late List> _positions; + + List _vendors = const []; + Vendor? _selectedVendor; + + @override + void initState() { + super.initState(); + _dateController = TextEditingController(text: widget.order.date); + _globalLocationController = TextEditingController( + text: widget.order.locationAddress, + ); + + _positions = >[ + { + 'role': widget.order.title, + 'count': widget.order.workersNeeded, + 'start_time': widget.order.startTime, + 'end_time': widget.order.endTime, + 'lunch_break': 0, + 'location': null, + }, + ]; + + // Mock vendors initialization + _vendors = const [ + Vendor( + id: 'v1', + name: 'Elite Staffing', + rates: { + 'Server': 25.0, + 'Bartender': 30.0, + 'Cook': 28.0, + 'Busser': 18.0, + 'Host': 20.0, + 'Barista': 22.0, + 'Dishwasher': 17.0, + 'Event Staff': 19.0, + }, + ), + Vendor( + id: 'v2', + name: 'Premier Workforce', + rates: { + 'Server': 22.0, + 'Bartender': 28.0, + 'Cook': 25.0, + 'Busser': 16.0, + 'Host': 18.0, + 'Barista': 20.0, + 'Dishwasher': 15.0, + 'Event Staff': 18.0, + }, + ), + ]; + _selectedVendor = _vendors.first; + } + + @override + void dispose() { + _dateController.dispose(); + _globalLocationController.dispose(); + super.dispose(); + } + + void _addPosition() { + setState(() { + _positions.add({ + 'role': '', + 'count': 1, + 'start_time': '09:00', + 'end_time': '17:00', + 'lunch_break': 0, + 'location': null, + }); + }); + } + + void _removePosition(int index) { + if (_positions.length > 1) { + setState(() => _positions.removeAt(index)); + } + } + + void _updatePosition(int index, String key, dynamic value) { + setState(() => _positions[index][key] = value); + } + + double _calculateTotalCost() { + double total = 0; + for (final Map pos in _positions) { + double hours = 8.0; + try { + final List startParts = pos['start_time'].toString().split(':'); + final List endParts = pos['end_time'].toString().split(':'); + final double startH = + int.parse(startParts[0]) + int.parse(startParts[1]) / 60; + final double endH = + int.parse(endParts[0]) + int.parse(endParts[1]) / 60; + hours = endH - startH; + if (hours < 0) hours += 24; + } catch (_) {} + + final double rate = + _selectedVendor?.rates[pos['role']] ?? widget.order.hourlyRate; + total += hours * rate * (pos['count'] as int); + } + return total; + } + + @override + Widget build(BuildContext context) { + if (_isLoading && _showReview) { + return _buildSuccessView(); + } + + return _showReview ? _buildReviewView() : _buildFormView(); + } + + Widget _buildFormView() { + return Container( + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + color: UiColors.bgPrimary, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + _buildHeader(), + Expanded( + child: ListView( + padding: const EdgeInsets.all(UiConstants.space5), + children: [ + Text( + 'Edit Your Order', + style: UiTypography.headline3m.textPrimary, + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('VENDOR'), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + ), + height: 48, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: _selectedVendor, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (Vendor? vendor) { + if (vendor != null) { + setState(() => _selectedVendor = vendor); + } + }, + items: _vendors.map((Vendor vendor) { + return DropdownMenuItem( + value: vendor, + child: Text( + vendor.name, + style: UiTypography.body2m.textPrimary, + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('DATE'), + UiTextField( + controller: _dateController, + hintText: 'mm/dd/yyyy', + prefixIcon: UiIcons.calendar, + readOnly: true, + onTap: () {}, + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('LOCATION'), + UiTextField( + controller: _globalLocationController, + hintText: 'Business address', + prefixIcon: UiIcons.mapPin, + ), + const SizedBox(height: UiConstants.space6), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'POSITIONS', + style: UiTypography.headline4m.textPrimary, + ), + TextButton( + onPressed: _addPosition, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(UiIcons.add, size: 16, color: UiColors.primary), + SizedBox(width: UiConstants.space2), + Text( + 'Add Position', + style: TextStyle( + color: UiColors.primary, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: UiConstants.space3), + + ..._positions.asMap().entries.map(( + MapEntry> entry, + ) { + return _buildPositionCard(entry.key, entry.value); + }), + + const SizedBox(height: 40), + ], + ), + ), + _buildBottomAction( + label: 'Review ${_positions.length} Positions', + onPressed: () => setState(() => _showReview = true), + ), + ], + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), + decoration: const BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.2), + borderRadius: UiConstants.radiusMd, + ), + child: const Icon( + UiIcons.chevronLeft, + color: UiColors.white, + size: 24, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'One-Time Order', + style: UiTypography.headline3m.copyWith(color: UiColors.white), + ), + Text( + 'Refine your staffing needs', + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text(title, style: UiTypography.footnote2r.textSecondary), + ); + } + + Widget _buildPositionCard(int index, Map pos) { + return Container( + margin: const EdgeInsets.only(bottom: UiConstants.space3), + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'POSITION #${index + 1}', + style: UiTypography.footnote1m.textSecondary, + ), + if (_positions.length > 1) + GestureDetector( + onTap: () => _removePosition(index), + child: Text( + 'Remove', + style: UiTypography.footnote1m.copyWith( + color: UiColors.destructive, + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space3), + + _buildDropdownField( + hint: 'Select role', + value: pos['role'], + items: [ + ...(_selectedVendor?.rates.keys.toList() ?? + [ + 'Server', + 'Bartender', + 'Cook', + 'Busser', + 'Host', + 'Barista', + 'Dishwasher', + 'Event Staff', + ]), + if (pos['role'] != null && + pos['role'].toString().isNotEmpty && + !(_selectedVendor?.rates.keys.contains(pos['role']) ?? false)) + pos['role'].toString(), + ], + itemBuilder: (dynamic role) { + final double? rate = _selectedVendor?.rates[role]; + if (rate == null) return role.toString(); + return '$role - \$${rate.toStringAsFixed(0)}/hr'; + }, + onChanged: (dynamic val) => _updatePosition(index, 'role', val), + ), + + const SizedBox(height: UiConstants.space3), + + Row( + children: [ + Expanded( + child: _buildInlineTimeInput( + label: 'Start', + value: pos['start_time'], + onTap: () {}, + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: _buildInlineTimeInput( + label: 'End', + value: pos['end_time'], + onTap: () {}, + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Workers', + style: UiTypography.footnote2r.textSecondary, + ), + const SizedBox(height: UiConstants.space1), + Container( + height: 40, + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusSm, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + if ((pos['count'] as int) > 1) { + _updatePosition( + index, + 'count', + (pos['count'] as int) - 1, + ); + } + }, + child: const Icon(UiIcons.minus, size: 12), + ), + Text( + '${pos['count']}', + style: UiTypography.body2b.textPrimary, + ), + GestureDetector( + onTap: () => _updatePosition( + index, + 'count', + (pos['count'] as int) + 1, + ), + child: const Icon(UiIcons.add, size: 12), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: UiConstants.space4), + + if (pos['location'] == null) + GestureDetector( + onTap: () => _updatePosition(index, 'location', ''), + child: Row( + children: [ + const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary), + const SizedBox(width: UiConstants.space1), + Text( + 'Use different location for this position', + style: UiTypography.footnote1m.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 14, + color: UiColors.iconSecondary, + ), + const SizedBox(width: UiConstants.space1), + Text( + 'Different Location', + style: UiTypography.footnote1m.textSecondary, + ), + ], + ), + GestureDetector( + onTap: () => _updatePosition(index, 'location', null), + child: const Icon( + UiIcons.close, + size: 14, + color: UiColors.destructive, + ), + ), + ], + ), + const SizedBox(height: UiConstants.space2), + UiTextField( + controller: TextEditingController(text: pos['location']), + hintText: 'Enter different address', + onChanged: (String val) => + _updatePosition(index, 'location', val), + ), + ], + ), + + const SizedBox(height: UiConstants.space3), + + _buildSectionHeader('LUNCH BREAK'), + _buildDropdownField( + hint: 'No break', + value: pos['lunch_break'], + items: [0, 15, 30, 45, 60], + itemBuilder: (dynamic val) { + if (val == 0) return 'No break'; + return '$val min'; + }, + onChanged: (dynamic val) => + _updatePosition(index, 'lunch_break', val), + ), + ], + ), + ); + } + + Widget _buildDropdownField({ + required String hint, + required dynamic value, + required List items, + String Function(dynamic)? itemBuilder, + required ValueChanged onChanged, + }) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + hint: Text(hint, style: UiTypography.body2r.textPlaceholder), + value: value == '' || value == null ? null : value, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: onChanged, + items: items.toSet().map((dynamic item) { + String label = item.toString(); + if (itemBuilder != null) label = itemBuilder(item); + return DropdownMenuItem( + value: item, + child: Text(label, style: UiTypography.body2r.textPrimary), + ); + }).toList(), + ), + ), + ); + } + + Widget _buildInlineTimeInput({ + required String label, + required String value, + required VoidCallback onTap, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: UiTypography.footnote2r.textSecondary), + const SizedBox(height: UiConstants.space1), + GestureDetector( + onTap: onTap, + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + borderRadius: UiConstants.radiusSm, + border: Border.all(color: UiColors.border), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value.isEmpty ? '--:--' : value, + style: UiTypography.body2r.textPrimary, + ), + const Icon( + UiIcons.clock, + size: 14, + color: UiColors.iconSecondary, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildBottomAction({ + required String label, + required VoidCallback onPressed, + }) { + return Container( + padding: EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + MediaQuery.of(context).padding.bottom + UiConstants.space5, + ), + decoration: const BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.border)), + ), + child: SizedBox( + width: double.infinity, + child: UiButton.primary( + text: label, + onPressed: onPressed, + size: UiButtonSize.large, + ), + ), + ); + } + + Widget _buildReviewView() { + final int totalWorkers = _positions.fold( + 0, + (int sum, Map p) => sum + (p['count'] as int), + ); + final double totalCost = _calculateTotalCost(); + + return Container( + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + _buildHeader(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Summary Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.primary.withValues(alpha: 0.05), + UiColors.primary.withValues(alpha: 0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: UiColors.primary.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildSummaryItem('${_positions.length}', 'Positions'), + _buildSummaryItem('$totalWorkers', 'Workers'), + _buildSummaryItem( + '\$${totalCost.round()}', + 'Est. Cost', + ), + ], + ), + ), + const SizedBox(height: 20), + + // Order Details + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: UiColors.separatorPrimary), + ), + child: Column( + children: [ + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 16, + color: UiColors.primary, + ), + const SizedBox(width: 8), + Text( + _dateController.text, + style: UiTypography.body2m.textPrimary, + ), + ], + ), + if (_globalLocationController + .text + .isNotEmpty) ...[ + const SizedBox(height: 12), + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 16, + color: UiColors.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _globalLocationController.text, + style: UiTypography.body2r.textPrimary, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ], + ), + ), + const SizedBox(height: 24), + + Text( + 'Positions Breakdown', + style: UiTypography.body2b.textPrimary, + ), + const SizedBox(height: 12), + + ..._positions.map( + (Map pos) => _buildReviewPositionCard(pos), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + + // Footer + Container( + padding: EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + MediaQuery.of(context).padding.bottom + UiConstants.space5, + ), + decoration: const BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.border)), + ), + child: Row( + children: [ + Expanded( + child: UiButton.secondary( + text: 'Edit', + onPressed: () => setState(() => _showReview = false), + ), + ), + const SizedBox(width: 12), + Expanded( + child: UiButton.primary( + text: 'Confirm & Save', + onPressed: () async { + setState(() => _isLoading = true); + await Future.delayed(const Duration(seconds: 1)); + if (mounted) Navigator.pop(context); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSummaryItem(String value, String label) { + return Column( + children: [ + Text( + value, + style: UiTypography.headline2m.copyWith( + color: UiColors.primary, + fontWeight: FontWeight.bold, + ), + ), + Text( + label.toUpperCase(), + style: UiTypography.titleUppercase4m.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ); + } + + Widget _buildReviewPositionCard(Map pos) { + final double rate = + _selectedVendor?.rates[pos['role']] ?? widget.order.hourlyRate; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: UiColors.separatorSecondary), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + pos['role'].toString().isEmpty + ? 'Position' + : pos['role'].toString(), + style: UiTypography.body2b.textPrimary, + ), + Text( + '${pos['count']} worker${pos['count'] > 1 ? 's' : ''}', + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + Text( + '\$${rate.round()}/hr', + style: UiTypography.body2b.copyWith(color: UiColors.primary), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon( + UiIcons.clock, + size: 14, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 6), + Text( + '${pos['start_time']} - ${pos['end_time']}', + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ], + ), + ); + } + + Widget _buildSuccessView() { + return Container( + width: double.infinity, + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: UiColors.accent, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + UiIcons.success, + size: 40, + color: UiColors.foreground, + ), + ), + ), + const SizedBox(height: 24), + Text( + 'Order Updated!', + style: UiTypography.headline1m.copyWith(color: UiColors.white), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Text( + 'Your shift has been updated successfully.', + textAlign: TextAlign.center, + style: UiTypography.body1r.copyWith( + color: UiColors.white.withValues(alpha: 0.7), + ), + ), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: UiButton.secondary( + text: 'Back to Orders', + fullWidth: true, + style: OutlinedButton.styleFrom( + backgroundColor: UiColors.white, + foregroundColor: UiColors.primary, + ), + onPressed: () => Navigator.pop(context), + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart b/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart new file mode 100644 index 00000000..32536f0b --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/src/view_orders_module.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_data_connect/krow_data_connect.dart'; + +import 'data/repositories/view_orders_repository_impl.dart'; +import 'domain/repositories/i_view_orders_repository.dart'; +import 'domain/usecases/get_orders_use_case.dart'; +import 'presentation/blocs/view_orders_cubit.dart'; +import 'presentation/pages/view_orders_page.dart'; + +/// Module for the View Orders feature. +/// +/// This module sets up Dependency Injection for repositories, use cases, +/// and BLoCs, and defines the feature's navigation routes. +class ViewOrdersModule extends Module { + @override + List get imports => [DataConnectModule()]; + + @override + void binds(Injector i) { + // Repositories + i.addLazySingleton( + () => ViewOrdersRepositoryImpl( + orderRepositoryMock: i.get(), + ), + ); + + // UseCases + i.addLazySingleton(GetOrdersUseCase.new); + + // BLoCs + i.addSingleton(ViewOrdersCubit.new); + } + + @override + void routes(RouteManager r) { + r.child('/', child: (BuildContext context) => const ViewOrdersPage()); + } +} diff --git a/apps/mobile/packages/features/client/view_orders/lib/view_orders.dart b/apps/mobile/packages/features/client/view_orders/lib/view_orders.dart new file mode 100644 index 00000000..87ab3a35 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/lib/view_orders.dart @@ -0,0 +1,3 @@ +library; + +export 'src/view_orders_module.dart'; diff --git a/apps/mobile/packages/features/client/view_orders/pubspec.yaml b/apps/mobile/packages/features/client/view_orders/pubspec.yaml new file mode 100644 index 00000000..5cbf0541 --- /dev/null +++ b/apps/mobile/packages/features/client/view_orders/pubspec.yaml @@ -0,0 +1,42 @@ +name: view_orders +description: Client View Orders feature package +publish_to: 'none' +version: 1.0.0+1 +resolution: workspace + +environment: + sdk: '>=3.10.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + + # Architecture + flutter_modular: ^6.3.2 + flutter_bloc: ^8.1.3 + equatable: ^2.0.5 + + # Shared packages + design_system: + path: ../../../design_system + core_localization: + path: ../../../core_localization + krow_domain: + path: ../../../domain + krow_core: + path: ../../../core + + # UI + lucide_icons: ^0.257.0 + intl: ^0.20.1 + url_launcher: ^6.3.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + bloc_test: ^9.1.5 + mocktail: ^1.0.1 + +flutter: + uses-material-design: true diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 903fdd09..22b2d153 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -1114,6 +1114,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: transitive description: diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml index 1f8f5ec3..33599721 100644 --- a/apps/mobile/pubspec.yaml +++ b/apps/mobile/pubspec.yaml @@ -15,6 +15,7 @@ workspace: - packages/features/client/settings - packages/features/client/hubs - packages/features/client/create_order + - packages/features/client/view_orders - packages/features/client/client_main - apps/staff - apps/client