From d928dfb645f6cb0c2a9fe972bc1857fb895daed7 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Fri, 23 Jan 2026 12:05:04 -0500 Subject: [PATCH] Refactor order creation and edit UI for consistency Refactored BLoC and widget code for order creation flows to improve code style, readability, and consistency. Unified the edit order bottom sheet to follow the Unified Order Flow prototype, supporting multiple positions, review, and confirmation steps. Updated UI components to use more concise widget tree structures and standardized button implementations. --- .../blocs/client_create_order_bloc.dart | 2 +- .../blocs/one_time_order_bloc.dart | 19 +- .../blocs/one_time_order_state.dart | 19 +- .../presentation/blocs/rapid_order_bloc.dart | 21 +- .../presentation/pages/create_order_page.dart | 5 +- .../create_order/create_order_view.dart | 95 ++- .../one_time_order/one_time_order_header.dart | 4 +- .../one_time_order_position_card.dart | 110 +-- .../one_time_order_section_header.dart | 26 +- .../one_time_order/one_time_order_view.dart | 40 +- .../presentation/widgets/order_type_card.dart | 10 +- .../rapid_order/rapid_order_example_card.dart | 5 +- .../rapid_order/rapid_order_header.dart | 6 +- .../rapid_order/rapid_order_success_view.dart | 10 +- .../widgets/rapid_order/rapid_order_view.dart | 45 +- .../presentation/widgets/view_order_card.dart | 673 ++++++++++++++++-- 16 files changed, 840 insertions(+), 250 deletions(-) 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..8ea45002 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,7 +8,7 @@ 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(_onDateChanged); on(_onLocationChanged); on(_onPositionAdded); @@ -37,13 +37,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: '', - )); + ), + ); emit(state.copyWith(positions: newPositions)); } @@ -83,10 +84,12 @@ class OneTimeOrderBloc extends Bloc { 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_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart index 2ef862f6..2f286262 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 @@ -17,12 +17,7 @@ class OneTimeOrderState extends Equatable { date: DateTime.now(), location: '', positions: const [ - OneTimeOrderPosition( - role: '', - count: 1, - startTime: '', - endTime: '', - ), + OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''), ], ); } @@ -50,10 +45,10 @@ class OneTimeOrderState extends Equatable { @override List get props => [ - date, - location, - positions, - status, - errorMessage, - ]; + date, + location, + positions, + status, + errorMessage, + ]; } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart 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..ec2797ac 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 @@ -99,8 +99,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 +114,27 @@ 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: + [ + '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(), ), ), ), @@ -153,7 +156,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 +176,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 +203,13 @@ class OneTimeOrderPositionCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ GestureDetector( - onTap: () => onUpdated(position.copyWith( + onTap: () => onUpdated( + position.copyWith( count: (position.count > 1) ? position.count - 1 - : 1)), + : 1, + ), + ), child: const Icon(UiIcons.minus, size: 12), ), Text( @@ -210,7 +218,8 @@ class OneTimeOrderPositionCard extends StatelessWidget { ), GestureDetector( onTap: () => onUpdated( - position.copyWith(count: position.count + 1)), + position.copyWith(count: position.count + 1), + ), child: const Icon(UiIcons.add, size: 12), ), ], @@ -249,11 +258,16 @@ class OneTimeOrderPositionCard extends StatelessWidget { children: [ Row( children: [ - const Icon(UiIcons.mapPin, - size: 14, color: UiColors.iconSecondary), + const Icon( + UiIcons.mapPin, + size: 14, + color: UiColors.iconSecondary, + ), const SizedBox(width: UiConstants.space1), Text( - t.client_create_order.one_time + t + .client_create_order + .one_time .different_location_title, style: UiTypography.footnote1m.textSecondary, ), @@ -283,10 +297,7 @@ class OneTimeOrderPositionCard extends StatelessWidget { 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, @@ -312,38 +323,45 @@ class OneTimeOrderPositionCard extends StatelessWidget { items: >[ DropdownMenuItem( value: 0, - child: Text(t.client_create_order.one_time.no_break, - style: UiTypography.body2r.textPrimary), + child: Text( + t.client_create_order.one_time.no_break, + style: UiTypography.body2r.textPrimary, + ), ), DropdownMenuItem( value: 10, child: Text( - '10 ${t.client_create_order.one_time.paid_break}', - style: UiTypography.body2r.textPrimary), + '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), + '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), + '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), + '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), + '60 ${t.client_create_order.one_time.unpaid_break}', + style: UiTypography.body2r.textPrimary, + ), ), ], ), 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..b8909ac6 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 @@ -58,8 +58,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()), ), ], ), @@ -90,34 +91,34 @@ class _OneTimeOrderForm extends StatelessWidget { 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( @@ -133,13 +134,14 @@ class _OneTimeOrderForm extends StatelessWidget { endLabel: labels.end_label, lunchLabel: labels.lunch_break_label, 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/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart index 93bb7175..b8704f21 100644 --- 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 @@ -715,7 +715,8 @@ class _ViewOrderCardState extends State { } } -/// A bottom sheet for editing an existing order. +/// A sophisticated bottom sheet for editing an existing order, +/// following the Unified Order Flow prototype. class _OrderEditSheet extends StatefulWidget { const _OrderEditSheet({required this.order}); @@ -726,37 +727,94 @@ class _OrderEditSheet extends StatefulWidget { } class _OrderEditSheetState extends State<_OrderEditSheet> { - late TextEditingController _titleController; + bool _showReview = false; + bool _isLoading = false; + late TextEditingController _dateController; - late TextEditingController _locationController; - late TextEditingController _workersNeededController; + late TextEditingController _globalLocationController; + + // Local state for positions (starts with the single position from OrderItem) + late List> _positions; @override void initState() { super.initState(); - _titleController = TextEditingController(text: widget.order.title); _dateController = TextEditingController(text: widget.order.date); - _locationController = TextEditingController( + _globalLocationController = TextEditingController( text: widget.order.locationAddress, ); - _workersNeededController = TextEditingController( - text: widget.order.workersNeeded.toString(), - ); + + _positions = >[ + { + 'role': widget.order.title, + 'count': widget.order.workersNeeded, + 'start_time': widget.order.startTime, + 'end_time': widget.order.endTime, + 'location': '', // Specific location if different from global + }, + ]; } @override void dispose() { - _titleController.dispose(); _dateController.dispose(); - _locationController.dispose(); - _workersNeededController.dispose(); + _globalLocationController.dispose(); super.dispose(); } + void _addPosition() { + setState(() { + _positions.add({ + 'role': '', + 'count': 1, + 'start_time': '09:00', + 'end_time': '17:00', + 'location': '', + }); + }); + } + + 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; // Default fallback + 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 (_) {} + total += hours * widget.order.hourlyRate * (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.9, + height: MediaQuery.of(context).size.height * 0.95, decoration: const BoxDecoration( color: UiColors.bgSecondary, borderRadius: BorderRadius.vertical( @@ -819,66 +877,289 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { // Content Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.all(UiConstants.space5), + padding: const EdgeInsets.all(20), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildSectionLabel('Position Title'), - UiTextField( - controller: _titleController, - hintText: 'e.g. Server, Bartender', - prefixIcon: UiIcons.briefcase, - ), - const SizedBox(height: UiConstants.space4), - - _buildSectionLabel('Date'), + _buildSectionLabel('Date *'), UiTextField( controller: _dateController, - hintText: 'Select Date', + hintText: 'mm/dd/yyyy', prefixIcon: UiIcons.calendar, readOnly: true, onTap: () { - // TODO: Show date picker + // TODO: Date picker }, ), - const SizedBox(height: UiConstants.space4), + const SizedBox(height: 16), - _buildSectionLabel('Location'), + _buildSectionLabel('Location *'), UiTextField( - controller: _locationController, + controller: _globalLocationController, hintText: 'Business address', prefixIcon: UiIcons.mapPin, ), - const SizedBox(height: UiConstants.space4), + const SizedBox(height: 24), - _buildSectionLabel('Workers Needed'), + // Positions Header Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: UiTextField( - controller: _workersNeededController, - hintText: 'Quantity', - prefixIcon: UiIcons.users, - keyboardType: TextInputType.number, + Text( + 'Positions', + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, ), ), + UiButton.text( + leadingIcon: UiIcons.add, + text: 'Add Position', + onPressed: _addPosition, + ), ], ), - const SizedBox(height: UiConstants.space6), + const SizedBox(height: 8), - UiButton.primary( - text: 'Save Changes', - fullWidth: true, - onPressed: () { - // TODO: Implement save logic - Navigator.pop(context); - }, + ..._positions.asMap().entries.map(( + MapEntry> entry, + ) { + return _buildPositionCard(entry.key, entry.value); + }), + + const SizedBox(height: 40), + ], + ), + ), + ), + + // Footer + Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.separatorPrimary)), + ), + child: SafeArea( + top: false, + child: UiButton.primary( + text: 'Review ${_positions.length} Positions', + fullWidth: true, + onPressed: () => setState(() => _showReview = true), + ), + ), + ), + ], + ), + ); + } + + 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(UiConstants.radiusBase * 2), + ), + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + decoration: const BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.vertical( + top: Radius.circular(UiConstants.radiusBase * 2), + ), + ), + child: Row( + children: [ + GestureDetector( + onTap: () => setState(() => _showReview = false), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon( + UiIcons.chevronLeft, + color: UiColors.white, + size: 24, + ), + ), ), - const SizedBox(height: UiConstants.space3), - UiButton.ghost( - text: 'Cancel', - fullWidth: true, - onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Review Order', + style: UiTypography.title1m.copyWith( + color: UiColors.white, + ), + ), + Text( + 'Confirm details before saving', + style: UiTypography.body3r.copyWith( + color: UiColors.white.withValues(alpha: 0.7), + ), + ), + ], + ), + ], + ), + ), + + // Content + 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.copyWith( + color: UiColors.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.copyWith( + color: UiColors.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ], + ), + ), + const SizedBox(height: 24), + + Text( + 'Positions Breakdown', + style: UiTypography.body2b.copyWith( + color: UiColors.textPrimary, + ), + ), + const SizedBox(height: 12), + + ..._positions.map( + (Map pos) => _buildReviewPositionCard(pos), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + + // Footer + Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.separatorPrimary)), + ), + child: SafeArea( + top: false, + 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) { + // TODO: Implement actual save logic + } + }, + ), ), ], ), @@ -889,6 +1170,298 @@ class _OrderEditSheetState extends State<_OrderEditSheet> { ); } + 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 _buildPositionCard(int index, Map pos) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: UiColors.separatorSecondary, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: UiColors.primary, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${index + 1}', + style: UiTypography.footnote2b.copyWith( + color: UiColors.white, + ), + ), + ), + ), + const SizedBox(width: 8), + Text( + 'Position ${index + 1}', + style: UiTypography.footnote2m.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + if (_positions.length > 1) + GestureDetector( + onTap: () => _removePosition(index), + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Color(0xFFFEF2F2), + shape: BoxShape.circle, + ), + child: const Icon( + UiIcons.close, + size: 14, + color: UiColors.destructive, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + _buildSectionLabel('Position Title *'), + UiTextField( + controller: TextEditingController(text: pos['role']), + hintText: 'e.g. Server, Bartender', + prefixIcon: UiIcons.briefcase, + onChanged: (String val) => _updatePosition(index, 'role', val), + ), + const SizedBox(height: 12), + + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionLabel('Start Time *'), + UiTextField( + controller: TextEditingController( + text: pos['start_time'], + ), + prefixIcon: UiIcons.clock, + onTap: () {}, // Time picker + ), + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionLabel('End Time *'), + UiTextField( + controller: TextEditingController(text: pos['end_time']), + prefixIcon: UiIcons.clock, + onTap: () {}, // Time picker + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + + _buildSectionLabel('Workers Needed'), + Row( + children: [ + _buildCounterBtn( + icon: UiIcons.minus, + onTap: () { + if ((pos['count'] as int) > 1) { + _updatePosition(index, 'count', (pos['count'] as int) - 1); + } + }, + ), + const SizedBox(width: 16), + Text('${pos['count']}', style: UiTypography.body1b), + const SizedBox(width: 16), + _buildCounterBtn( + icon: UiIcons.add, + onTap: () { + _updatePosition(index, 'count', (pos['count'] as int) + 1); + }, + ), + ], + ), + ], + ), + ); + } + + Widget _buildReviewPositionCard(Map pos) { + // Simplified cost calculation + 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.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + '${pos['count']} worker${pos['count'] > 1 ? 's' : ''}', + style: UiTypography.footnote2r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + Text( + '\$${widget.order.hourlyRate.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.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildCounterBtn({ + required IconData icon, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 16, color: UiColors.primary), + ), + ); + } + + 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), + ), + ), + ], + ), + ); + } + Widget _buildSectionLabel(String label) { return Padding( padding: const EdgeInsets.only(bottom: 8),