Refactor create order UI and improve architecture

Refactored the client create order feature to move UI logic for order type selection into a dedicated CreateOrderView widget, extracted order type UI metadata to a separate entity, and updated page widgets to delegate to new view components. Improved code documentation and structure for arguments, use cases, and repository interfaces. Added new localization keys and minor design system icon update.
This commit is contained in:
Achintha Isuru
2026-01-23 01:55:12 -05:00
parent e24019494c
commit 9e513d96af
36 changed files with 1451 additions and 1113 deletions

View File

@@ -280,13 +280,17 @@
"end_label": "End",
"workers_label": "Workers",
"lunch_break_label": "Lunch Break",
"no_break": "No break",
"paid_break": "min (Paid)",
"unpaid_break": "min (Unpaid)",
"different_location": "Use different location for this position",
"different_location_title": "Different Location",
"different_location_hint": "Enter different address",
"create_order": "Create Order",
"creating": "Creating...",
"success_title": "Order Created!",
"success_message": "Your shift request has been posted. Workers will start applying soon."
"success_message": "Your shift request has been posted. Workers will start applying soon.",
"back_to_orders": "Back to Orders"
},
"recurring": {
"title": "Recurring Order",

View File

@@ -286,7 +286,8 @@
"create_order": "Crear Orden",
"creating": "Creando...",
"success_title": "¡Orden Creada!",
"success_message": "Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto."
"success_message": "Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto.",
"back_to_orders": "Volver a Órdenes"
},
"recurring": {
"title": "Orden Recurrente",

View File

@@ -78,6 +78,9 @@ class UiIcons {
/// Chevron left icon
static const IconData chevronLeft = _IconLib.chevronLeft;
/// Chevron down icon
static const IconData chevronDown = _IconLib.chevronDown;
// --- Status & Feedback ---
/// Info icon

View File

@@ -1,3 +1,4 @@
library client_create_order;
/// Library for the Client Create Order feature.
library;
export 'src/create_order_module.dart';

View File

@@ -29,7 +29,9 @@ class ClientCreateOrderModule extends Module {
// Repositories
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
() => ClientCreateOrderRepositoryImpl(
orderMock: i.get<OrderRepositoryMock>()),
orderMock: i.get<OrderRepositoryMock>(),
dataConnect: ExampleConnector.instance,
),
);
// UseCases

View File

@@ -4,30 +4,40 @@ import '../../domain/repositories/client_create_order_repository_interface.dart'
/// Implementation of [ClientCreateOrderRepositoryInterface].
///
/// This implementation delegates all data access to the Data Connect layer,
/// specifically using [OrderRepositoryMock] for now as per the platform's mocking strategy.
/// This implementation coordinates data access for order creation by delegating
/// to the [OrderRepositoryMock] and [ExampleConnector] from the shared
/// Data Connect package.
///
/// It follows the KROW Clean Architecture by keeping the data layer focused
/// on delegation and data mapping, without business logic.
class ClientCreateOrderRepositoryImpl
implements ClientCreateOrderRepositoryInterface {
/// Creates a [ClientCreateOrderRepositoryImpl].
///
/// Requires an [OrderRepositoryMock] from the Data Connect shared package.
ClientCreateOrderRepositoryImpl({required OrderRepositoryMock orderMock})
: _orderMock = orderMock;
/// Requires the [OrderRepositoryMock] from the shared Data Connect package.
/// TODO: Inject and use ExampleConnector when real mutations are available.
ClientCreateOrderRepositoryImpl({
required OrderRepositoryMock orderMock,
@Deprecated('Use ExampleConnector for real mutations in the future')
Object? dataConnect,
}) : _orderMock = orderMock;
final OrderRepositoryMock _orderMock;
@override
Future<List<OrderType>> getOrderTypes() {
// Delegates to Data Connect layer
return _orderMock.getOrderTypes();
}
@override
Future<void> createOneTimeOrder(OneTimeOrder order) {
// Delegates to Data Connect layer
return _orderMock.createOneTimeOrder(order);
}
@override
Future<void> createRapidOrder(String description) {
// Delegates to Data Connect layer
return _orderMock.createRapidOrder(description);
}
}

View File

@@ -2,9 +2,15 @@ import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
/// Represents the arguments required for the [CreateOneTimeOrderUseCase].
///
/// Encapsulates the [OneTimeOrder] details required to create a new
/// one-time staffing request.
class OneTimeOrderArguments extends UseCaseArgument {
/// Creates a [OneTimeOrderArguments] instance.
///
/// Requires the [order] details.
const OneTimeOrderArguments({required this.order});
/// The order details to be created.
final OneTimeOrder order;

View File

@@ -1,9 +1,15 @@
import 'package:krow_core/core.dart';
/// Represents the arguments required for the [CreateRapidOrderUseCase].
///
/// Encapsulates the text description of the urgent staffing need
/// for rapid order creation.
class RapidOrderArguments extends UseCaseArgument {
/// Creates a [RapidOrderArguments] instance.
///
/// Requires the [description] of the staffing need.
const RapidOrderArguments({required this.description});
/// The text description of the urgent staffing need.
final String description;

View File

@@ -2,15 +2,23 @@ import 'package:krow_domain/krow_domain.dart';
/// Interface for the Client Create Order repository.
///
/// This repository handles the retrieval of available order types and the
/// submission of different types of staffing orders (Rapid, One-Time, etc.).
/// This repository is responsible for:
/// 1. Retrieving available order types for the client.
/// 2. Submitting different types of staffing orders (Rapid, One-Time).
///
/// It follows the KROW Clean Architecture by defining the contract in the
/// domain layer, to be implemented in the data layer.
abstract interface class ClientCreateOrderRepositoryInterface {
/// Retrieves the list of available order types.
/// Retrieves the list of available order types (e.g., Rapid, One-Time, Recurring).
Future<List<OrderType>> getOrderTypes();
/// Submits a one-time staffing order.
/// Submits a one-time staffing order with specific details.
///
/// [order] contains the date, location, and required positions.
Future<void> createOneTimeOrder(OneTimeOrder order);
/// Submits a rapid (urgent) staffing order with a text description.
/// Submits a rapid (urgent) staffing order via a text description.
///
/// [description] is the text message (or transcribed voice) describing the need.
Future<void> createRapidOrder(String description);
}

View File

@@ -4,12 +4,14 @@ import '../repositories/client_create_order_repository_interface.dart';
/// Use case for creating a one-time staffing order.
///
/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit
/// a [OneTimeOrder] provided via [OneTimeOrderArguments].
/// This use case encapsulates the logic for submitting a structured
/// staffing request and delegates the data operation to the
/// [ClientCreateOrderRepositoryInterface].
class CreateOneTimeOrderUseCase
implements UseCase<OneTimeOrderArguments, void> {
/// Creates a [CreateOneTimeOrderUseCase].
///
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
const CreateOneTimeOrderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;

View File

@@ -4,11 +4,12 @@ import '../repositories/client_create_order_repository_interface.dart';
/// Use case for creating a rapid (urgent) staffing order.
///
/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit
/// a text-based urgent request via [RapidOrderArguments].
/// This use case handles urgent, text-based staffing requests and
/// delegates the submission to the [ClientCreateOrderRepositoryInterface].
class CreateRapidOrderUseCase implements UseCase<RapidOrderArguments, void> {
/// Creates a [CreateRapidOrderUseCase].
///
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
const CreateRapidOrderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;

View File

@@ -4,11 +4,12 @@ import '../repositories/client_create_order_repository_interface.dart';
/// Use case for retrieving the available order types for a client.
///
/// This use case interacts with the [ClientCreateOrderRepositoryInterface] to
/// fetch the list of staffing order types (e.g., Rapid, One-Time).
/// This use case fetches the list of supported staffing order types
/// from the [ClientCreateOrderRepositoryInterface].
class GetOrderTypesUseCase implements NoInputUseCase<List<OrderType>> {
/// Creates a [GetOrderTypesUseCase].
///
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
const GetOrderTypesUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;

View File

@@ -7,7 +7,6 @@ import 'client_create_order_state.dart';
/// BLoC for managing the list of available order types.
class ClientCreateOrderBloc
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
ClientCreateOrderBloc(this._getOrderTypesUseCase)
: super(const ClientCreateOrderInitial()) {
on<ClientCreateOrderTypesRequested>(_onTypesRequested);

View File

@@ -12,7 +12,6 @@ class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent {
}
class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent {
const ClientCreateOrderTypeSelected(this.typeId);
final String typeId;

View File

@@ -16,8 +16,8 @@ class ClientCreateOrderInitial extends ClientCreateOrderState {
/// State representing successfully loaded order types from the repository.
class ClientCreateOrderLoadSuccess extends ClientCreateOrderState {
const ClientCreateOrderLoadSuccess(this.orderTypes);
/// The list of available order types retrieved from the domain.
final List<OrderType> orderTypes;

View File

@@ -7,7 +7,6 @@ import 'one_time_order_state.dart';
/// BLoC for managing the multi-step one-time order creation form.
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
: super(OneTimeOrderState.initial()) {
on<OneTimeOrderDateChanged>(_onDateChanged);

View File

@@ -6,7 +6,6 @@ import 'rapid_order_state.dart';
/// BLoC for managing the rapid (urgent) order creation flow.
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
RapidOrderBloc(this._createRapidOrderUseCase)
: super(
const RapidOrderInitial(

View File

@@ -8,7 +8,6 @@ abstract class RapidOrderEvent extends Equatable {
}
class RapidOrderMessageChanged extends RapidOrderEvent {
const RapidOrderMessageChanged(this.message);
final String message;
@@ -25,7 +24,6 @@ class RapidOrderSubmitted extends RapidOrderEvent {
}
class RapidOrderExampleSelected extends RapidOrderEvent {
const RapidOrderExampleSelected(this.example);
final String example;

View File

@@ -8,7 +8,6 @@ abstract class RapidOrderState extends Equatable {
}
class RapidOrderInitial extends RapidOrderState {
const RapidOrderInitial({
this.message = '',
this.isListening = false,
@@ -43,7 +42,6 @@ class RapidOrderSuccess extends RapidOrderState {
}
class RapidOrderFailure extends RapidOrderState {
const RapidOrderFailure(this.error);
final String error;

View File

@@ -1,39 +1,15 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/client_create_order_bloc.dart';
import '../blocs/client_create_order_event.dart';
import '../blocs/client_create_order_state.dart';
import '../navigation/client_create_order_navigator.dart';
import '../widgets/order_type_card.dart';
/// Helper to map keys to localized strings.
String _getTranslation({required String key}) {
if (key == 'client_create_order.types.rapid') {
return t.client_create_order.types.rapid;
} else if (key == 'client_create_order.types.rapid_desc') {
return t.client_create_order.types.rapid_desc;
} else if (key == 'client_create_order.types.one_time') {
return t.client_create_order.types.one_time;
} else if (key == 'client_create_order.types.one_time_desc') {
return t.client_create_order.types.one_time_desc;
} else if (key == 'client_create_order.types.recurring') {
return t.client_create_order.types.recurring;
} else if (key == 'client_create_order.types.recurring_desc') {
return t.client_create_order.types.recurring_desc;
} else if (key == 'client_create_order.types.permanent') {
return t.client_create_order.types.permanent;
} else if (key == 'client_create_order.types.permanent_desc') {
return t.client_create_order.types.permanent_desc;
}
return key;
}
import '../widgets/create_order/create_order_view.dart';
/// Main entry page for the client create order flow.
/// Allows the user to select the type of order they want to create.
///
/// This page initializes the [ClientCreateOrderBloc] and displays the [CreateOrderView].
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
/// delegating its state and UI to other components.
class ClientCreateOrderPage extends StatelessWidget {
/// Creates a [ClientCreateOrderPage].
const ClientCreateOrderPage({super.key});
@@ -43,191 +19,7 @@ class ClientCreateOrderPage extends StatelessWidget {
return BlocProvider<ClientCreateOrderBloc>(
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
..add(const ClientCreateOrderTypesRequested()),
child: const _CreateOrderView(),
child: const CreateOrderView(),
);
}
}
class _CreateOrderView extends StatelessWidget {
const _CreateOrderView();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: t.client_create_order.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space6),
child: Text(
t.client_create_order.section_title,
style: UiTypography.footnote1m.copyWith(
color: UiColors.textDescription,
letterSpacing: 0.5,
),
),
),
Expanded(
child:
BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
builder:
(BuildContext context, ClientCreateOrderState state) {
if (state is ClientCreateOrderLoadSuccess) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: UiConstants.space4,
crossAxisSpacing: UiConstants.space4,
childAspectRatio: 1,
),
itemCount: state.orderTypes.length,
itemBuilder: (BuildContext context, int index) {
final OrderType type = state.orderTypes[index];
final _OrderTypeUiMetadata ui =
_OrderTypeUiMetadata.fromId(id: type.id);
return OrderTypeCard(
icon: ui.icon,
title: _getTranslation(key: type.titleKey),
description: _getTranslation(
key: type.descriptionKey,
),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.descriptionColor,
onTap: () {
switch (type.id) {
case 'rapid':
Modular.to.pushRapidOrder();
break;
case 'one-time':
Modular.to.pushOneTimeOrder();
break;
case 'recurring':
Modular.to.pushRecurringOrder();
break;
case 'permanent':
Modular.to.pushPermanentOrder();
break;
}
},
);
},
);
}
return const Center(child: CircularProgressIndicator());
},
),
),
],
),
),
),
);
}
}
/// Metadata for styling order type cards based on their ID.
class _OrderTypeUiMetadata {
const _OrderTypeUiMetadata({
required this.icon,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
});
/// Factory to get metadata based on order type ID.
factory _OrderTypeUiMetadata.fromId({required String id}) {
switch (id) {
case 'rapid':
return const _OrderTypeUiMetadata(
icon: UiIcons.zap,
backgroundColor: UiColors.tagPending,
borderColor: UiColors.separatorSpecial,
iconBackgroundColor: UiColors.textWarning,
iconColor: UiColors.white,
textColor: UiColors.textWarning,
descriptionColor: UiColors.textWarning,
);
case 'one-time':
return const _OrderTypeUiMetadata(
icon: UiIcons.calendar,
backgroundColor: UiColors.tagInProgress,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
);
case 'recurring':
return const _OrderTypeUiMetadata(
icon: UiIcons.rotateCcw,
backgroundColor: UiColors.tagSuccess,
borderColor: UiColors.switchActive,
iconBackgroundColor: UiColors.textSuccess,
iconColor: UiColors.white,
textColor: UiColors.textSuccess,
descriptionColor: UiColors.textSuccess,
);
case 'permanent':
return const _OrderTypeUiMetadata(
icon: UiIcons.briefcase,
backgroundColor: UiColors.tagRefunded,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
);
default:
return const _OrderTypeUiMetadata(
icon: UiIcons.help,
backgroundColor: UiColors.bgSecondary,
borderColor: UiColors.border,
iconBackgroundColor: UiColors.iconSecondary,
iconColor: UiColors.white,
textColor: UiColors.textPrimary,
descriptionColor: UiColors.textSecondary,
);
}
}
/// Icon for the order type.
final IconData icon;
/// Background color for the card.
final Color backgroundColor;
/// Border color for the card.
final Color borderColor;
/// Background color for the icon.
final Color iconBackgroundColor;
/// Color for the icon.
final Color iconColor;
/// Color for the title text.
final Color textColor;
/// Color for the description text.
final Color descriptionColor;
}

View File

@@ -1,20 +1,15 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/one_time_order_bloc.dart';
import '../blocs/one_time_order_event.dart';
import '../blocs/one_time_order_state.dart';
import '../widgets/one_time_order/one_time_order_date_picker.dart';
import '../widgets/one_time_order/one_time_order_location_input.dart';
import '../widgets/one_time_order/one_time_order_position_card.dart';
import '../widgets/one_time_order/one_time_order_section_header.dart';
import '../widgets/one_time_order/one_time_order_success_view.dart';
import '../widgets/one_time_order/one_time_order_view.dart';
/// Page for creating a one-time staffing order.
/// Users can specify the date, location, and multiple staff positions required.
///
/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView].
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
/// delegating its state and UI to other components.
class OneTimeOrderPage extends StatelessWidget {
/// Creates a [OneTimeOrderPage].
const OneTimeOrderPage({super.key});
@@ -23,186 +18,7 @@ class OneTimeOrderPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider<OneTimeOrderBloc>(
create: (BuildContext context) => Modular.get<OneTimeOrderBloc>(),
child: const _OneTimeOrderView(),
child: const OneTimeOrderView(),
);
}
}
class _OneTimeOrderView extends StatelessWidget {
const _OneTimeOrderView();
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return BlocBuilder<OneTimeOrderBloc, OneTimeOrderState>(
builder: (BuildContext context, OneTimeOrderState state) {
if (state.status == OneTimeOrderStatus.success) {
return OneTimeOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: 'Done',
onDone: () => Modular.to.pop(),
);
}
return Scaffold(
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: labels.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: Stack(
children: <Widget>[
_OneTimeOrderForm(state: state),
if (state.status == OneTimeOrderStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
bottomNavigationBar: _BottomActionButton(
label: labels.create_order,
isLoading: state.status == OneTimeOrderStatus.loading,
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(const OneTimeOrderSubmitted()),
),
);
},
);
}
}
class _OneTimeOrderForm extends StatelessWidget {
const _OneTimeOrderForm({required this.state});
final OneTimeOrderState state;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
OneTimeOrderSectionHeader(title: labels.create_your_order),
const SizedBox(height: UiConstants.space4),
OneTimeOrderDatePicker(
label: labels.date_label,
value: state.date,
onChanged: (DateTime date) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderDateChanged(date)),
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderLocationInput(
label: labels.location_label,
value: state.location,
onChanged: (String location) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderLocationChanged(location)),
),
const SizedBox(height: UiConstants.space6),
OneTimeOrderSectionHeader(
title: labels.positions_title,
actionLabel: labels.add_position,
onAction: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(const OneTimeOrderPositionAdded()),
),
const SizedBox(height: UiConstants.space4),
// Positions List
...state.positions
.asMap()
.entries
.map((MapEntry<int, OneTimeOrderPosition> entry) {
final int index = entry.key;
final OneTimeOrderPosition position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: OneTimeOrderPositionCard(
index: index,
position: position,
isRemovable: state.positions.length > 1,
positionLabel: labels.positions_title,
roleLabel: labels.select_role,
workersLabel: labels.workers_label,
startLabel: labels.start_label,
endLabel: labels.end_label,
lunchLabel: labels.lunch_break_label,
onUpdated: (OneTimeOrderPosition updated) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(index, updated),
);
},
onRemoved: () {
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderPositionRemoved(index));
},
),
);
}),
const SizedBox(height: 100), // Space for bottom button
],
);
}
}
class _BottomActionButton extends StatelessWidget {
const _BottomActionButton({
required this.label,
required this.onPressed,
this.isLoading = false,
});
final String label;
final VoidCallback onPressed;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: UiConstants.space5,
right: UiConstants.space5,
top: UiConstants.space4,
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space4,
),
decoration: BoxDecoration(
color: UiColors.white,
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -4),
),
],
),
child: isLoading
? const UiButton(
buttonBuilder: _dummyBuilder,
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: UiColors.primary, strokeWidth: 2),
),
)
: UiButton.primary(
text: label,
onPressed: onPressed,
size: UiButtonSize.large,
),
);
}
static Widget _dummyBuilder(
BuildContext context,
VoidCallback? onPressed,
ButtonStyle? style,
Widget child,
) {
return Center(child: child);
}
}

View File

@@ -1,18 +1,15 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import '../blocs/rapid_order_bloc.dart';
import '../blocs/rapid_order_event.dart';
import '../blocs/rapid_order_state.dart';
import '../widgets/rapid_order/rapid_order_example_card.dart';
import '../widgets/rapid_order/rapid_order_header.dart';
import '../widgets/rapid_order/rapid_order_success_view.dart';
import '../widgets/rapid_order/rapid_order_view.dart';
/// Rapid Order Flow Page - Emergency staffing requests.
/// Features voice recognition simulation and quick example selection.
///
/// This page initializes the [RapidOrderBloc] and displays the [RapidOrderView].
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
/// delegating its state and UI to other components.
class RapidOrderPage extends StatelessWidget {
/// Creates a [RapidOrderPage].
const RapidOrderPage({super.key});
@@ -21,306 +18,7 @@ class RapidOrderPage extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider<RapidOrderBloc>(
create: (BuildContext context) => Modular.get<RapidOrderBloc>(),
child: const _RapidOrderView(),
);
}
}
class _RapidOrderView extends StatelessWidget {
const _RapidOrderView();
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
return BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderSuccess) {
return RapidOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: labels.back_to_orders,
onDone: () => Modular.to.pop(),
);
}
return const _RapidOrderForm();
},
);
}
}
class _RapidOrderForm extends StatefulWidget {
const _RapidOrderForm();
@override
State<_RapidOrderForm> createState() => _RapidOrderFormState();
}
class _RapidOrderFormState extends State<_RapidOrderForm> {
final TextEditingController _messageController = TextEditingController();
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
final DateTime now = DateTime.now();
final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now);
final String timeStr = DateFormat('h:mm a').format(now);
return BlocListener<RapidOrderBloc, RapidOrderState>(
listener: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderInitial) {
if (_messageController.text != state.message) {
_messageController.text = state.message;
_messageController.selection = TextSelection.fromPosition(
TextPosition(offset: _messageController.text.length),
);
}
}
},
child: Scaffold(
backgroundColor: UiColors.bgPrimary,
body: Column(
children: <Widget>[
RapidOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
date: dateStr,
time: timeStr,
onBack: () => Modular.to.pop(),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
labels.tell_us,
style: UiTypography.headline3m.textPrimary,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1,
),
decoration: BoxDecoration(
color: UiColors.destructive,
borderRadius: UiConstants.radiusSm,
),
child: Text(
labels.urgent_badge,
style: UiTypography.footnote2b.copyWith(
color: UiColors.white,
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
// Main Card
Container(
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
final RapidOrderInitial? initialState =
state is RapidOrderInitial ? state : null;
final bool isSubmitting =
state is RapidOrderSubmitting;
return Column(
children: <Widget>[
// Icon
_AnimatedZapIcon(),
const SizedBox(height: UiConstants.space4),
Text(
labels.need_staff,
style: UiTypography.headline2m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
labels.type_or_speak,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space6),
// Examples
if (initialState != null)
...initialState.examples
.asMap()
.entries
.map((MapEntry<int, String> entry) {
final int index = entry.key;
final String example = entry.value;
final bool isHighlighted = index == 0;
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space2),
child: RapidOrderExampleCard(
example: example,
isHighlighted: isHighlighted,
label: labels.example,
onTap: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
RapidOrderExampleSelected(example),
),
),
);
}),
const SizedBox(height: UiConstants.space4),
// Input
TextField(
controller: _messageController,
maxLines: 4,
onChanged: (String value) {
BlocProvider.of<RapidOrderBloc>(context).add(
RapidOrderMessageChanged(value),
);
},
decoration: InputDecoration(
hintText: labels.hint,
hintStyle: UiTypography.body2r.copyWith(
color: UiColors.textPlaceholder,
),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(
color: UiColors.border,
),
),
contentPadding:
const EdgeInsets.all(UiConstants.space4),
),
),
const SizedBox(height: UiConstants.space4),
// Actions
_RapidOrderActions(
labels: labels,
isSubmitting: isSubmitting,
isListening: initialState?.isListening ?? false,
isMessageEmpty: initialState != null &&
initialState.message.trim().isEmpty,
),
],
);
},
),
),
],
),
),
),
],
),
),
);
}
}
class _AnimatedZapIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.destructive.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
UiIcons.zap,
color: UiColors.white,
size: 32,
),
);
}
}
class _RapidOrderActions extends StatelessWidget {
const _RapidOrderActions({
required this.labels,
required this.isSubmitting,
required this.isListening,
required this.isMessageEmpty,
});
final TranslationsClientCreateOrderRapidEn labels;
final bool isSubmitting;
final bool isListening;
final bool isMessageEmpty;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: UiButton.secondary(
text: isListening ? labels.listening : labels.speak,
leadingIcon: UiIcons.bell, // Placeholder for mic
onPressed: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderVoiceToggled(),
),
style: OutlinedButton.styleFrom(
backgroundColor: isListening
? UiColors.destructive.withValues(alpha: 0.05)
: null,
side: isListening
? const BorderSide(color: UiColors.destructive)
: null,
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: UiButton.primary(
text: isSubmitting ? labels.sending : labels.send,
trailingIcon: UiIcons.arrowRight,
onPressed: isSubmitting || isMessageEmpty
? null
: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderSubmitted(),
),
),
),
],
child: const RapidOrderView(),
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/widgets.dart';
/// Metadata for styling order type cards based on their ID.
class OrderTypeUiMetadata {
/// Creates an [OrderTypeUiMetadata].
const OrderTypeUiMetadata({
required this.icon,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
});
/// Factory to get metadata based on order type ID.
factory OrderTypeUiMetadata.fromId({required String id}) {
switch (id) {
case 'rapid':
return const OrderTypeUiMetadata(
icon: UiIcons.zap,
backgroundColor: UiColors.tagPending,
borderColor: UiColors.separatorSpecial,
iconBackgroundColor: UiColors.textWarning,
iconColor: UiColors.white,
textColor: UiColors.textWarning,
descriptionColor: UiColors.textWarning,
);
case 'one-time':
return const OrderTypeUiMetadata(
icon: UiIcons.calendar,
backgroundColor: UiColors.tagInProgress,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
);
case 'recurring':
return const OrderTypeUiMetadata(
icon: UiIcons.rotateCcw,
backgroundColor: UiColors.tagSuccess,
borderColor: UiColors.switchActive,
iconBackgroundColor: UiColors.textSuccess,
iconColor: UiColors.white,
textColor: UiColors.textSuccess,
descriptionColor: UiColors.textSuccess,
);
case 'permanent':
return const OrderTypeUiMetadata(
icon: UiIcons.briefcase,
backgroundColor: UiColors.tagRefunded,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
);
default:
return const OrderTypeUiMetadata(
icon: UiIcons.help,
backgroundColor: UiColors.bgSecondary,
borderColor: UiColors.border,
iconBackgroundColor: UiColors.iconSecondary,
iconColor: UiColors.white,
textColor: UiColors.textPrimary,
descriptionColor: UiColors.textSecondary,
);
}
}
/// Icon for the order type.
final IconData icon;
/// Background color for the card.
final Color backgroundColor;
/// Border color for the card.
final Color borderColor;
/// Background color for the icon.
final Color iconBackgroundColor;
/// Color for the icon.
final Color iconColor;
/// Color for the title text.
final Color textColor;
/// Color for the description text.
final Color descriptionColor;
}

View File

@@ -0,0 +1,129 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../blocs/client_create_order_bloc.dart';
import '../../blocs/client_create_order_state.dart';
import '../../navigation/client_create_order_navigator.dart';
import '../../ui_entities/order_type_ui_metadata.dart';
import '../order_type_card.dart';
/// Helper to map keys to localized strings.
String _getTranslation({required String key}) {
if (key == 'client_create_order.types.rapid') {
return t.client_create_order.types.rapid;
} else if (key == 'client_create_order.types.rapid_desc') {
return t.client_create_order.types.rapid_desc;
} else if (key == 'client_create_order.types.one_time') {
return t.client_create_order.types.one_time;
} else if (key == 'client_create_order.types.one_time_desc') {
return t.client_create_order.types.one_time_desc;
} else if (key == 'client_create_order.types.recurring') {
return t.client_create_order.types.recurring;
} else if (key == 'client_create_order.types.recurring_desc') {
return t.client_create_order.types.recurring_desc;
} else if (key == 'client_create_order.types.permanent') {
return t.client_create_order.types.permanent;
} else if (key == 'client_create_order.types.permanent_desc') {
return t.client_create_order.types.permanent_desc;
}
return key;
}
/// The main content of the Create Order page.
class CreateOrderView extends StatelessWidget {
/// Creates a [CreateOrderView].
const CreateOrderView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: t.client_create_order.title,
onLeadingPressed: () => Modular.to.pop(),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space6),
child: Text(
t.client_create_order.section_title,
style: UiTypography.footnote1m.copyWith(
color: UiColors.textDescription,
letterSpacing: 0.5,
),
),
),
Expanded(
child:
BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
builder:
(BuildContext context, ClientCreateOrderState state) {
if (state is ClientCreateOrderLoadSuccess) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: UiConstants.space4,
crossAxisSpacing: UiConstants.space4,
childAspectRatio: 1,
),
itemCount: state.orderTypes.length,
itemBuilder: (BuildContext context, int index) {
final OrderType type = state.orderTypes[index];
final OrderTypeUiMetadata ui =
OrderTypeUiMetadata.fromId(id: type.id);
return OrderTypeCard(
icon: ui.icon,
title: _getTranslation(key: type.titleKey),
description: _getTranslation(
key: type.descriptionKey,
),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.descriptionColor,
onTap: () {
switch (type.id) {
case 'rapid':
Modular.to.pushRapidOrder();
break;
case 'one-time':
Modular.to.pushOneTimeOrder();
break;
case 'recurring':
Modular.to.pushRecurringOrder();
break;
case 'permanent':
Modular.to.pushPermanentOrder();
break;
}
},
);
},
);
}
return const Center(child: CircularProgressIndicator());
},
),
),
],
),
),
),
);
}
}

View File

@@ -3,7 +3,16 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
/// A date picker field for the one-time order form.
class OneTimeOrderDatePicker extends StatelessWidget {
/// Matches the prototype input field style.
class OneTimeOrderDatePicker extends StatefulWidget {
/// Creates a [OneTimeOrderDatePicker].
const OneTimeOrderDatePicker({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
/// The label text to display above the field.
final String label;
@@ -13,56 +22,53 @@ class OneTimeOrderDatePicker extends StatelessWidget {
/// Callback when a new date is selected.
final ValueChanged<DateTime> onChanged;
/// Creates a [OneTimeOrderDatePicker].
const OneTimeOrderDatePicker({
required this.label,
required this.value,
required this.onChanged,
super.key,
});
@override
State<OneTimeOrderDatePicker> createState() => _OneTimeOrderDatePickerState();
}
class _OneTimeOrderDatePickerState extends State<OneTimeOrderDatePicker> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(
text: DateFormat('yyyy-MM-dd').format(widget.value),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(OneTimeOrderDatePicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != oldWidget.value) {
_controller.text = DateFormat('yyyy-MM-dd').format(widget.value);
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space2),
InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: value,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
onChanged(picked);
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3 + 2,
),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
child: Row(
children: <Widget>[
const Icon(UiIcons.calendar,
size: 20, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(
DateFormat('EEEE, MMM d, yyyy').format(value),
style: UiTypography.body1r.textPrimary,
),
],
),
),
),
],
return UiTextField(
label: widget.label,
controller: _controller,
readOnly: true,
prefixIcon: UiIcons.calendar,
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: widget.value,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) {
widget.onChanged(picked);
}
},
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the one-time order flow with a colored background.
class OneTimeOrderHeader extends StatelessWidget {
/// Creates a [OneTimeOrderHeader].
const OneTimeOrderHeader({
required this.title,
required this.subtitle,
required this.onBack,
super.key,
});
/// The title of the page.
final String title;
/// The subtitle or description.
final String subtitle;
/// Callback when the back button is pressed.
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + UiConstants.space5,
bottom: UiConstants.space5,
left: UiConstants.space5,
right: UiConstants.space5,
),
color: UiColors.primary,
child: Row(
children: <Widget>[
GestureDetector(
onTap: onBack,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(
color: UiColors.white,
),
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
);
}
}

View File

@@ -2,16 +2,8 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A location input field for the one-time order form.
class OneTimeOrderLocationInput extends StatelessWidget {
/// The label text to display above the field.
final String label;
/// The current location value.
final String value;
/// Callback when the location text changes.
final ValueChanged<String> onChanged;
/// Matches the prototype input field style.
class OneTimeOrderLocationInput extends StatefulWidget {
/// Creates a [OneTimeOrderLocationInput].
const OneTimeOrderLocationInput({
required this.label,
@@ -20,14 +12,50 @@ class OneTimeOrderLocationInput extends StatelessWidget {
super.key,
});
/// The label text to display above the field.
final String label;
/// The current location value.
final String value;
/// Callback when the location value changes.
final ValueChanged<String> onChanged;
@override
State<OneTimeOrderLocationInput> createState() =>
_OneTimeOrderLocationInputState();
}
class _OneTimeOrderLocationInputState extends State<OneTimeOrderLocationInput> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(OneTimeOrderLocationInput oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != _controller.text) {
_controller.text = widget.value;
}
}
@override
Widget build(BuildContext context) {
return UiTextField(
label: label,
hintText: 'Select Branch/Location',
controller: TextEditingController(text: value)
..selection = TextSelection.collapsed(offset: value.length),
onChanged: onChanged,
label: widget.label,
controller: _controller,
onChanged: widget.onChanged,
hintText: 'Enter address',
prefixIcon: UiIcons.mapPin,
);
}

View File

@@ -1,9 +1,27 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
/// A card widget for editing a specific position in a one-time order.
/// Matches the prototype layout while using design system tokens.
class OneTimeOrderPositionCard extends StatelessWidget {
/// Creates a [OneTimeOrderPositionCard].
const OneTimeOrderPositionCard({
required this.index,
required this.position,
required this.isRemovable,
required this.onUpdated,
required this.onRemoved,
required this.positionLabel,
required this.roleLabel,
required this.workersLabel,
required this.startLabel,
required this.endLabel,
required this.lunchLabel,
super.key,
});
/// The index of the position in the list.
final int index;
@@ -37,22 +55,6 @@ class OneTimeOrderPositionCard extends StatelessWidget {
/// Label for the lunch break.
final String lunchLabel;
/// Creates a [OneTimeOrderPositionCard].
const OneTimeOrderPositionCard({
required this.index,
required this.position,
required this.isRemovable,
required this.onUpdated,
required this.onRemoved,
required this.positionLabel,
required this.roleLabel,
required this.workersLabel,
required this.startLabel,
required this.endLabel,
required this.lunchLabel,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
@@ -61,13 +63,6 @@ class OneTimeOrderPositionCard extends StatelessWidget {
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -77,149 +72,281 @@ class OneTimeOrderPositionCard extends StatelessWidget {
children: <Widget>[
Text(
'$positionLabel #${index + 1}',
style: UiTypography.body1b.textPrimary,
style: UiTypography.footnote1m.textSecondary,
),
if (isRemovable)
IconButton(
icon: const Icon(UiIcons.delete,
size: 20, color: UiColors.destructive),
onPressed: onRemoved,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
GestureDetector(
onTap: onRemoved,
child: Text(
t.client_create_order.one_time.remove,
style: UiTypography.footnote1m.copyWith(
color: UiColors.destructive,
),
),
),
],
),
const Divider(height: UiConstants.space6),
const SizedBox(height: UiConstants.space3),
// Role (Dropdown)
_LabelField(
label: roleLabel,
child: DropdownButtonFormField<String>(
value: position.role.isEmpty ? null : position.role,
items: <String>['Server', 'Bartender', 'Cook', 'Busser', 'Host']
.map((String role) => DropdownMenuItem<String>(
value: role,
child:
Text(role, style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (String? val) {
if (val != null) {
onUpdated(position.copyWith(role: val));
}
},
decoration: _inputDecoration(UiIcons.briefcase),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 44,
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
hint:
Text(roleLabel, style: UiTypography.body2r.textPlaceholder),
value: position.role.isEmpty ? null : position.role,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (String? val) {
if (val != null) {
onUpdated(position.copyWith(role: val));
}
},
items: <String>[
'Server',
'Bartender',
'Cook',
'Busser',
'Host',
'Barista',
'Dishwasher',
'Event Staff'
].map((String role) {
// Mock rates for UI matching
final int rate = _getMockRate(role);
return DropdownMenuItem<String>(
value: role,
child: Text(
'$role - \$$rate/hr',
style: UiTypography.body2r.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space3),
// Start/End/Workers Row
Row(
children: <Widget>[
// Start Time
Expanded(
child: _buildTimeInput(
context: context,
label: startLabel,
value: position.startTime,
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null && context.mounted) {
onUpdated(
position.copyWith(startTime: picked.format(context)));
}
},
),
),
const SizedBox(width: UiConstants.space2),
// End Time
Expanded(
child: _buildTimeInput(
context: context,
label: endLabel,
value: position.endTime,
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null && context.mounted) {
onUpdated(
position.copyWith(endTime: picked.format(context)));
}
},
),
),
const SizedBox(width: UiConstants.space2),
// Workers Count
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
workersLabel,
style: UiTypography.footnote2r.textSecondary,
),
const SizedBox(height: UiConstants.space1),
Container(
height: 40,
decoration: BoxDecoration(
color: UiColors.bgSecondary,
borderRadius: UiConstants.radiusSm,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
GestureDetector(
onTap: () => onUpdated(position.copyWith(
count: (position.count > 1)
? position.count - 1
: 1)),
child: const Icon(UiIcons.minus, size: 12),
),
Text(
'${position.count}',
style: UiTypography.body2b.textPrimary,
),
GestureDetector(
onTap: () => onUpdated(
position.copyWith(count: position.count + 1)),
child: const Icon(UiIcons.add, size: 12),
),
],
),
),
],
),
),
],
),
const SizedBox(height: UiConstants.space4),
// Count (Counter)
_LabelField(
label: workersLabel,
child: Row(
// Optional Location Override
if (position.location == null)
GestureDetector(
onTap: () => onUpdated(position.copyWith(location: '')),
child: Row(
children: <Widget>[
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: <Widget>[
_CounterButton(
icon: UiIcons.minus,
onPressed: position.count > 1
? () => onUpdated(
position.copyWith(count: position.count - 1))
: null,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
const Icon(UiIcons.mapPin,
size: 14, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Text(
t.client_create_order.one_time
.different_location_title,
style: UiTypography.footnote1m.textSecondary,
),
],
),
GestureDetector(
onTap: () => onUpdated(position.copyWith(location: null)),
child: const Icon(
UiIcons.close,
size: 14,
color: UiColors.destructive,
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4),
child: Text('${position.count}',
style: UiTypography.headline3m.textPrimary),
),
_CounterButton(
icon: UiIcons.add,
onPressed: () =>
onUpdated(position.copyWith(count: position.count + 1)),
const SizedBox(height: UiConstants.space2),
_PositionLocationInput(
value: position.location ?? '',
onChanged: (String val) =>
onUpdated(position.copyWith(location: val)),
hintText:
t.client_create_order.one_time.different_location_hint,
),
],
),
),
const SizedBox(height: UiConstants.space4),
// Start/End Time
Row(
children: <Widget>[
Expanded(
child: _LabelField(
label: startLabel,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 9, minute: 0),
);
if (picked != null) {
onUpdated(position.copyWith(
startTime: picked.format(context)));
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.startTime.isEmpty
? '--:--'
: position.startTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _LabelField(
label: endLabel,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 17, minute: 0),
);
if (picked != null) {
onUpdated(
position.copyWith(endTime: picked.format(context)));
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.endTime.isEmpty ? '--:--' : position.endTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
const SizedBox(height: UiConstants.space3),
// Lunch Break
_LabelField(
label: lunchLabel,
child: DropdownButtonFormField<int>(
value: position.lunchBreak,
items: <int>[0, 30, 45, 60]
.map((int mins) => DropdownMenuItem<int>(
value: mins,
child: Text('${mins}m',
style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (int? val) {
if (val != null) {
onUpdated(position.copyWith(lunchBreak: val));
}
},
decoration: _inputDecoration(UiIcons.clock),
Text(
lunchLabel,
style: UiTypography.footnote2r.textSecondary,
),
const SizedBox(height: UiConstants.space1),
Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<int>(
isExpanded: true,
value: position.lunchBreak,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (int? val) {
if (val != null) {
onUpdated(position.copyWith(lunchBreak: val));
}
},
items: <DropdownMenuItem<int>>[
DropdownMenuItem<int>(
value: 0,
child: Text(t.client_create_order.one_time.no_break,
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 10,
child: Text(
'10 ${t.client_create_order.one_time.paid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 15,
child: Text(
'15 ${t.client_create_order.one_time.paid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 30,
child: Text(
'30 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 45,
child: Text(
'45 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 60,
child: Text(
'60 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
],
),
),
),
],
@@ -227,68 +354,89 @@ class OneTimeOrderPositionCard extends StatelessWidget {
);
}
InputDecoration _inputDecoration(IconData icon) => InputDecoration(
prefixIcon: Icon(icon, size: 18, color: UiColors.iconSecondary),
contentPadding:
const EdgeInsets.symmetric(horizontal: UiConstants.space3),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(color: UiColors.border),
),
);
BoxDecoration _boxDecoration() => BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
);
}
class _LabelField extends StatelessWidget {
const _LabelField({required this.label, required this.child});
final String label;
final Widget child;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space1),
child,
],
Widget _buildTimeInput({
required BuildContext context,
required String label,
required String value,
required VoidCallback onTap,
}) {
return UiTextField(
label: label,
controller: TextEditingController(text: value),
readOnly: true,
onTap: onTap,
hintText: '--:--',
);
}
int _getMockRate(String role) {
switch (role) {
case 'Server':
return 18;
case 'Bartender':
return 22;
case 'Cook':
return 20;
case 'Busser':
return 16;
case 'Host':
return 17;
case 'Barista':
return 16;
case 'Dishwasher':
return 15;
case 'Event Staff':
return 20;
default:
return 15;
}
}
}
class _CounterButton extends StatelessWidget {
const _CounterButton({required this.icon, this.onPressed});
final IconData icon;
final VoidCallback? onPressed;
class _PositionLocationInput extends StatefulWidget {
const _PositionLocationInput({
required this.value,
required this.hintText,
required this.onChanged,
});
final String value;
final String hintText;
final ValueChanged<String> onChanged;
@override
State<_PositionLocationInput> createState() => _PositionLocationInputState();
}
class _PositionLocationInputState extends State<_PositionLocationInput> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(_PositionLocationInput oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != _controller.text) {
_controller.text = widget.value;
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border.all(
color: onPressed != null
? UiColors.border
: UiColors.border.withOpacity(0.5)),
borderRadius: UiConstants.radiusLg,
color: onPressed != null ? UiColors.white : UiColors.background,
),
child: Icon(
icon,
size: 16,
color: onPressed != null
? UiColors.iconPrimary
: UiColors.iconSecondary.withOpacity(0.5),
),
),
return UiTextField(
controller: _controller,
onChanged: widget.onChanged,
hintText: widget.hintText,
);
}
}

View File

@@ -3,6 +3,14 @@ import 'package:flutter/material.dart';
/// A header widget for sections in the one-time order form.
class OneTimeOrderSectionHeader extends StatelessWidget {
/// Creates a [OneTimeOrderSectionHeader].
const OneTimeOrderSectionHeader({
required this.title,
this.actionLabel,
this.onAction,
super.key,
});
/// The title text for the section.
final String title;
@@ -12,14 +20,6 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
/// Callback when the action button is tapped.
final VoidCallback? onAction;
/// Creates a [OneTimeOrderSectionHeader].
const OneTimeOrderSectionHeader({
required this.title,
this.actionLabel,
this.onAction,
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
@@ -27,14 +27,11 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
children: <Widget>[
Text(title, style: UiTypography.headline4m.textPrimary),
if (actionLabel != null && onAction != null)
TextButton.icon(
UiButton.text(
onPressed: onAction,
icon: const Icon(UiIcons.add, size: 16, color: UiColors.primary),
label: Text(actionLabel!, style: UiTypography.body2b.textPrimary),
style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: UiConstants.space2),
),
leadingIcon: UiIcons.add,
text: actionLabel!,
iconSize: 16,
),
],
);

View File

@@ -2,7 +2,17 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A view to display when a one-time order has been successfully created.
/// Matches the prototype success view layout with a gradient background and centered card.
class OneTimeOrderSuccessView extends StatelessWidget {
/// Creates a [OneTimeOrderSuccessView].
const OneTimeOrderSuccessView({
required this.title,
required this.message,
required this.buttonLabel,
required this.onDone,
super.key,
});
/// The title of the success message.
final String title;
@@ -15,54 +25,78 @@ class OneTimeOrderSuccessView extends StatelessWidget {
/// Callback when the completion button is tapped.
final VoidCallback onDone;
/// Creates a [OneTimeOrderSuccessView].
const OneTimeOrderSuccessView({
required this.title,
required this.message,
required this.buttonLabel,
required this.onDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.white,
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: UiColors.tagSuccess,
shape: BoxShape.circle,
),
child: const Icon(UiIcons.check,
size: 50, color: UiColors.textSuccess),
body: Container(
width: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[UiColors.primary, UiColors.buttonPrimaryHover],
),
),
child: SafeArea(
child: Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 40),
padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg * 1.5,
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
const SizedBox(height: UiConstants.space8),
Text(
title,
style: UiTypography.headline2m.textPrimary,
textAlign: TextAlign.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
color: UiColors.accent,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
UiIcons.check,
color: UiColors.black,
size: 32,
),
),
),
const SizedBox(height: UiConstants.space6),
Text(
title,
style: UiTypography.headline2m.textPrimary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space3),
Text(
message,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary.copyWith(
height: 1.5,
),
),
const SizedBox(height: UiConstants.space8),
SizedBox(
width: double.infinity,
child: UiButton.primary(
text: buttonLabel,
onPressed: onDone,
size: UiButtonSize.large,
),
),
],
),
const SizedBox(height: UiConstants.space4),
Text(
message,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space10),
UiButton.primary(
text: buttonLabel,
onPressed: onDone,
size: UiButtonSize.large,
),
],
),
),
),
),

View File

@@ -0,0 +1,185 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../blocs/one_time_order_bloc.dart';
import '../../blocs/one_time_order_event.dart';
import '../../blocs/one_time_order_state.dart';
import 'one_time_order_date_picker.dart';
import 'one_time_order_header.dart';
import 'one_time_order_location_input.dart';
import 'one_time_order_position_card.dart';
import 'one_time_order_section_header.dart';
import 'one_time_order_success_view.dart';
/// The main content of the One-Time Order page.
class OneTimeOrderView extends StatelessWidget {
/// Creates a [OneTimeOrderView].
const OneTimeOrderView({super.key});
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return BlocBuilder<OneTimeOrderBloc, OneTimeOrderState>(
builder: (BuildContext context, OneTimeOrderState state) {
if (state.status == OneTimeOrderStatus.success) {
return OneTimeOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: labels.back_to_orders,
onDone: () => Modular.to.pop(),
);
}
return Scaffold(
backgroundColor: UiColors.bgPrimary,
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: () => Modular.to.pop(),
),
Expanded(
child: Stack(
children: <Widget>[
_OneTimeOrderForm(state: state),
if (state.status == OneTimeOrderStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
_BottomActionButton(
label: state.status == OneTimeOrderStatus.loading
? labels.creating
: labels.create_order,
isLoading: state.status == OneTimeOrderStatus.loading,
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(const OneTimeOrderSubmitted()),
),
],
),
);
},
);
}
}
class _OneTimeOrderForm extends StatelessWidget {
const _OneTimeOrderForm({required this.state});
final OneTimeOrderState state;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
Text(
labels.create_your_order,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderDatePicker(
label: labels.date_label,
value: state.date,
onChanged: (DateTime date) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderDateChanged(date)),
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderLocationInput(
label: labels.location_label,
value: state.location,
onChanged: (String location) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderLocationChanged(location)),
),
const SizedBox(height: UiConstants.space6),
OneTimeOrderSectionHeader(
title: labels.positions_title,
actionLabel: labels.add_position,
onAction: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(const OneTimeOrderPositionAdded()),
),
const SizedBox(height: UiConstants.space3),
// Positions List
...state.positions
.asMap()
.entries
.map((MapEntry<int, OneTimeOrderPosition> entry) {
final int index = entry.key;
final OneTimeOrderPosition position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: OneTimeOrderPositionCard(
index: index,
position: position,
isRemovable: state.positions.length > 1,
positionLabel: labels.positions_title,
roleLabel: labels.select_role,
workersLabel: labels.workers_label,
startLabel: labels.start_label,
endLabel: labels.end_label,
lunchLabel: labels.lunch_break_label,
onUpdated: (OneTimeOrderPosition updated) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(index, updated),
);
},
onRemoved: () {
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderPositionRemoved(index));
},
),
);
}),
],
);
}
}
class _BottomActionButton extends StatelessWidget {
const _BottomActionButton({
required this.label,
required this.onPressed,
this.isLoading = false,
});
final String label;
final VoidCallback onPressed;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: UiConstants.space5,
right: UiConstants.space5,
top: UiConstants.space5,
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5,
),
decoration: const BoxDecoration(
color: UiColors.white,
border: Border(top: BorderSide(color: UiColors.border)),
),
child: SizedBox(
width: double.infinity,
child: UiButton.primary(
text: label,
onPressed: isLoading ? null : onPressed,
size: UiButtonSize.large,
),
),
);
}
}

View File

@@ -3,6 +3,21 @@ import 'package:flutter/material.dart';
/// A card widget representing an order type in the creation flow.
class OrderTypeCard extends StatelessWidget {
/// Creates an [OrderTypeCard].
const OrderTypeCard({
required this.icon,
required this.title,
required this.description,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
required this.onTap,
super.key,
});
/// Icon to display at the top of the card.
final IconData icon;
@@ -33,21 +48,6 @@ class OrderTypeCard extends StatelessWidget {
/// Callback when the card is tapped.
final VoidCallback onTap;
/// Creates an [OrderTypeCard].
const OrderTypeCard({
required this.icon,
required this.title,
required this.description,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return GestureDetector(

View File

@@ -3,6 +3,15 @@ import 'package:flutter/material.dart';
/// A card displaying an example message for a rapid order.
class RapidOrderExampleCard extends StatelessWidget {
/// Creates a [RapidOrderExampleCard].
const RapidOrderExampleCard({
required this.example,
required this.isHighlighted,
required this.label,
required this.onTap,
super.key,
});
/// The example text.
final String example;
@@ -15,15 +24,6 @@ class RapidOrderExampleCard extends StatelessWidget {
/// Callback when the card is tapped.
final VoidCallback onTap;
/// Creates a [RapidOrderExampleCard].
const RapidOrderExampleCard({
required this.example,
required this.isHighlighted,
required this.label,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return GestureDetector(

View File

@@ -3,6 +3,16 @@ import 'package:flutter/material.dart';
/// A header widget for the rapid order flow with a gradient background.
class RapidOrderHeader extends StatelessWidget {
/// Creates a [RapidOrderHeader].
const RapidOrderHeader({
required this.title,
required this.subtitle,
required this.date,
required this.time,
required this.onBack,
super.key,
});
/// The title of the page.
final String title;
@@ -18,16 +28,6 @@ class RapidOrderHeader extends StatelessWidget {
/// Callback when the back button is pressed.
final VoidCallback onBack;
/// Creates a [RapidOrderHeader].
const RapidOrderHeader({
required this.title,
required this.subtitle,
required this.date,
required this.time,
required this.onBack,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(

View File

@@ -3,6 +3,15 @@ import 'package:flutter/material.dart';
/// A view to display when a rapid order has been successfully created.
class RapidOrderSuccessView extends StatelessWidget {
/// Creates a [RapidOrderSuccessView].
const RapidOrderSuccessView({
required this.title,
required this.message,
required this.buttonLabel,
required this.onDone,
super.key,
});
/// The title of the success message.
final String title;
@@ -15,15 +24,6 @@ class RapidOrderSuccessView extends StatelessWidget {
/// Callback when the completion button is tapped.
final VoidCallback onDone;
/// Creates a [RapidOrderSuccessView].
const RapidOrderSuccessView({
required this.title,
required this.message,
required this.buttonLabel,
required this.onDone,
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@@ -0,0 +1,302 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import '../../blocs/rapid_order_bloc.dart';
import '../../blocs/rapid_order_event.dart';
import '../../blocs/rapid_order_state.dart';
import 'rapid_order_example_card.dart';
import 'rapid_order_header.dart';
import 'rapid_order_success_view.dart';
/// The main content of the Rapid Order page.
class RapidOrderView extends StatelessWidget {
/// Creates a [RapidOrderView].
const RapidOrderView({super.key});
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
return BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderSuccess) {
return RapidOrderSuccessView(
title: labels.success_title,
message: labels.success_message,
buttonLabel: labels.back_to_orders,
onDone: () => Modular.to.pop(),
);
}
return const _RapidOrderForm();
},
);
}
}
class _RapidOrderForm extends StatefulWidget {
const _RapidOrderForm();
@override
State<_RapidOrderForm> createState() => _RapidOrderFormState();
}
class _RapidOrderFormState extends State<_RapidOrderForm> {
final TextEditingController _messageController = TextEditingController();
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels =
t.client_create_order.rapid;
final DateTime now = DateTime.now();
final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now);
final String timeStr = DateFormat('h:mm a').format(now);
return BlocListener<RapidOrderBloc, RapidOrderState>(
listener: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderInitial) {
if (_messageController.text != state.message) {
_messageController.text = state.message;
_messageController.selection = TextSelection.fromPosition(
TextPosition(offset: _messageController.text.length),
);
}
}
},
child: Scaffold(
backgroundColor: UiColors.bgPrimary,
body: Column(
children: <Widget>[
RapidOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
date: dateStr,
time: timeStr,
onBack: () => Modular.to.pop(),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
labels.tell_us,
style: UiTypography.headline3m.textPrimary,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: UiConstants.space1,
),
decoration: BoxDecoration(
color: UiColors.destructive,
borderRadius: UiConstants.radiusSm,
),
child: Text(
labels.urgent_badge,
style: UiTypography.footnote2b.copyWith(
color: UiColors.white,
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
// Main Card
Container(
padding: const EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
final RapidOrderInitial? initialState =
state is RapidOrderInitial ? state : null;
final bool isSubmitting =
state is RapidOrderSubmitting;
return Column(
children: <Widget>[
// Icon
const _AnimatedZapIcon(),
const SizedBox(height: UiConstants.space4),
Text(
labels.need_staff,
style: UiTypography.headline2m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
labels.type_or_speak,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space6),
// Examples
if (initialState != null)
...initialState.examples
.asMap()
.entries
.map((MapEntry<int, String> entry) {
final int index = entry.key;
final String example = entry.value;
final bool isHighlighted = index == 0;
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space2),
child: RapidOrderExampleCard(
example: example,
isHighlighted: isHighlighted,
label: labels.example,
onTap: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
RapidOrderExampleSelected(example),
),
),
);
}),
const SizedBox(height: UiConstants.space4),
// Input
UiTextField(
controller: _messageController,
maxLines: 4,
onChanged: (String value) {
BlocProvider.of<RapidOrderBloc>(context).add(
RapidOrderMessageChanged(value),
);
},
hintText: labels.hint,
),
const SizedBox(height: UiConstants.space4),
// Actions
_RapidOrderActions(
labels: labels,
isSubmitting: isSubmitting,
isListening: initialState?.isListening ?? false,
isMessageEmpty: initialState != null &&
initialState.message.trim().isEmpty,
),
],
);
},
),
),
],
),
),
),
],
),
),
);
}
}
class _AnimatedZapIcon extends StatelessWidget {
const _AnimatedZapIcon();
@override
Widget build(BuildContext context) {
return Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.destructive.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Icon(
UiIcons.zap,
color: UiColors.white,
size: 32,
),
);
}
}
class _RapidOrderActions extends StatelessWidget {
const _RapidOrderActions({
required this.labels,
required this.isSubmitting,
required this.isListening,
required this.isMessageEmpty,
});
final TranslationsClientCreateOrderRapidEn labels;
final bool isSubmitting;
final bool isListening;
final bool isMessageEmpty;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: UiButton.secondary(
text: isListening ? labels.listening : labels.speak,
leadingIcon: UiIcons.bell, // Placeholder for mic
onPressed: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderVoiceToggled(),
),
style: OutlinedButton.styleFrom(
backgroundColor: isListening
? UiColors.destructive.withValues(alpha: 0.05)
: null,
side: isListening
? const BorderSide(color: UiColors.destructive)
: null,
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: UiButton.primary(
text: isSubmitting ? labels.sending : labels.send,
trailingIcon: UiIcons.arrowRight,
onPressed: isSubmitting || isMessageEmpty
? null
: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderSubmitted(),
),
),
),
],
);
}
}