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:
@@ -280,13 +280,17 @@
|
|||||||
"end_label": "End",
|
"end_label": "End",
|
||||||
"workers_label": "Workers",
|
"workers_label": "Workers",
|
||||||
"lunch_break_label": "Lunch Break",
|
"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": "Use different location for this position",
|
||||||
"different_location_title": "Different Location",
|
"different_location_title": "Different Location",
|
||||||
"different_location_hint": "Enter different address",
|
"different_location_hint": "Enter different address",
|
||||||
"create_order": "Create Order",
|
"create_order": "Create Order",
|
||||||
"creating": "Creating...",
|
"creating": "Creating...",
|
||||||
"success_title": "Order Created!",
|
"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": {
|
"recurring": {
|
||||||
"title": "Recurring Order",
|
"title": "Recurring Order",
|
||||||
|
|||||||
@@ -286,7 +286,8 @@
|
|||||||
"create_order": "Crear Orden",
|
"create_order": "Crear Orden",
|
||||||
"creating": "Creando...",
|
"creating": "Creando...",
|
||||||
"success_title": "¡Orden Creada!",
|
"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": {
|
"recurring": {
|
||||||
"title": "Orden Recurrente",
|
"title": "Orden Recurrente",
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ class UiIcons {
|
|||||||
/// Chevron left icon
|
/// Chevron left icon
|
||||||
static const IconData chevronLeft = _IconLib.chevronLeft;
|
static const IconData chevronLeft = _IconLib.chevronLeft;
|
||||||
|
|
||||||
|
/// Chevron down icon
|
||||||
|
static const IconData chevronDown = _IconLib.chevronDown;
|
||||||
|
|
||||||
// --- Status & Feedback ---
|
// --- Status & Feedback ---
|
||||||
|
|
||||||
/// Info icon
|
/// Info icon
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
library client_create_order;
|
/// Library for the Client Create Order feature.
|
||||||
|
library;
|
||||||
|
|
||||||
export 'src/create_order_module.dart';
|
export 'src/create_order_module.dart';
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ class ClientCreateOrderModule extends Module {
|
|||||||
// Repositories
|
// Repositories
|
||||||
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
|
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
|
||||||
() => ClientCreateOrderRepositoryImpl(
|
() => ClientCreateOrderRepositoryImpl(
|
||||||
orderMock: i.get<OrderRepositoryMock>()),
|
orderMock: i.get<OrderRepositoryMock>(),
|
||||||
|
dataConnect: ExampleConnector.instance,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
|
|||||||
@@ -4,30 +4,40 @@ import '../../domain/repositories/client_create_order_repository_interface.dart'
|
|||||||
|
|
||||||
/// Implementation of [ClientCreateOrderRepositoryInterface].
|
/// Implementation of [ClientCreateOrderRepositoryInterface].
|
||||||
///
|
///
|
||||||
/// This implementation delegates all data access to the Data Connect layer,
|
/// This implementation coordinates data access for order creation by delegating
|
||||||
/// specifically using [OrderRepositoryMock] for now as per the platform's mocking strategy.
|
/// 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
|
class ClientCreateOrderRepositoryImpl
|
||||||
implements ClientCreateOrderRepositoryInterface {
|
implements ClientCreateOrderRepositoryInterface {
|
||||||
|
|
||||||
/// Creates a [ClientCreateOrderRepositoryImpl].
|
/// Creates a [ClientCreateOrderRepositoryImpl].
|
||||||
///
|
///
|
||||||
/// Requires an [OrderRepositoryMock] from the Data Connect shared package.
|
/// Requires the [OrderRepositoryMock] from the shared Data Connect package.
|
||||||
ClientCreateOrderRepositoryImpl({required OrderRepositoryMock orderMock})
|
/// TODO: Inject and use ExampleConnector when real mutations are available.
|
||||||
: _orderMock = orderMock;
|
ClientCreateOrderRepositoryImpl({
|
||||||
|
required OrderRepositoryMock orderMock,
|
||||||
|
@Deprecated('Use ExampleConnector for real mutations in the future')
|
||||||
|
Object? dataConnect,
|
||||||
|
}) : _orderMock = orderMock;
|
||||||
final OrderRepositoryMock _orderMock;
|
final OrderRepositoryMock _orderMock;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<OrderType>> getOrderTypes() {
|
Future<List<OrderType>> getOrderTypes() {
|
||||||
|
// Delegates to Data Connect layer
|
||||||
return _orderMock.getOrderTypes();
|
return _orderMock.getOrderTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> createOneTimeOrder(OneTimeOrder order) {
|
Future<void> createOneTimeOrder(OneTimeOrder order) {
|
||||||
|
// Delegates to Data Connect layer
|
||||||
return _orderMock.createOneTimeOrder(order);
|
return _orderMock.createOneTimeOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> createRapidOrder(String description) {
|
Future<void> createRapidOrder(String description) {
|
||||||
|
// Delegates to Data Connect layer
|
||||||
return _orderMock.createRapidOrder(description);
|
return _orderMock.createRapidOrder(description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,15 @@ import 'package:krow_core/core.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Represents the arguments required for the [CreateOneTimeOrderUseCase].
|
/// Represents the arguments required for the [CreateOneTimeOrderUseCase].
|
||||||
|
///
|
||||||
|
/// Encapsulates the [OneTimeOrder] details required to create a new
|
||||||
|
/// one-time staffing request.
|
||||||
class OneTimeOrderArguments extends UseCaseArgument {
|
class OneTimeOrderArguments extends UseCaseArgument {
|
||||||
|
/// Creates a [OneTimeOrderArguments] instance.
|
||||||
|
///
|
||||||
|
/// Requires the [order] details.
|
||||||
const OneTimeOrderArguments({required this.order});
|
const OneTimeOrderArguments({required this.order});
|
||||||
|
|
||||||
/// The order details to be created.
|
/// The order details to be created.
|
||||||
final OneTimeOrder order;
|
final OneTimeOrder order;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
/// Represents the arguments required for the [CreateRapidOrderUseCase].
|
/// Represents the arguments required for the [CreateRapidOrderUseCase].
|
||||||
|
///
|
||||||
|
/// Encapsulates the text description of the urgent staffing need
|
||||||
|
/// for rapid order creation.
|
||||||
class RapidOrderArguments extends UseCaseArgument {
|
class RapidOrderArguments extends UseCaseArgument {
|
||||||
|
/// Creates a [RapidOrderArguments] instance.
|
||||||
|
///
|
||||||
|
/// Requires the [description] of the staffing need.
|
||||||
const RapidOrderArguments({required this.description});
|
const RapidOrderArguments({required this.description});
|
||||||
|
|
||||||
/// The text description of the urgent staffing need.
|
/// The text description of the urgent staffing need.
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,23 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
|
|
||||||
/// Interface for the Client Create Order repository.
|
/// Interface for the Client Create Order repository.
|
||||||
///
|
///
|
||||||
/// This repository handles the retrieval of available order types and the
|
/// This repository is responsible for:
|
||||||
/// submission of different types of staffing orders (Rapid, One-Time, etc.).
|
/// 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 {
|
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();
|
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);
|
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);
|
Future<void> createRapidOrder(String description);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import '../repositories/client_create_order_repository_interface.dart';
|
|||||||
|
|
||||||
/// Use case for creating a one-time staffing order.
|
/// Use case for creating a one-time staffing order.
|
||||||
///
|
///
|
||||||
/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit
|
/// This use case encapsulates the logic for submitting a structured
|
||||||
/// a [OneTimeOrder] provided via [OneTimeOrderArguments].
|
/// staffing request and delegates the data operation to the
|
||||||
|
/// [ClientCreateOrderRepositoryInterface].
|
||||||
class CreateOneTimeOrderUseCase
|
class CreateOneTimeOrderUseCase
|
||||||
implements UseCase<OneTimeOrderArguments, void> {
|
implements UseCase<OneTimeOrderArguments, void> {
|
||||||
|
|
||||||
/// Creates a [CreateOneTimeOrderUseCase].
|
/// Creates a [CreateOneTimeOrderUseCase].
|
||||||
|
///
|
||||||
|
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
|
||||||
const CreateOneTimeOrderUseCase(this._repository);
|
const CreateOneTimeOrderUseCase(this._repository);
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import '../repositories/client_create_order_repository_interface.dart';
|
|||||||
|
|
||||||
/// Use case for creating a rapid (urgent) staffing order.
|
/// Use case for creating a rapid (urgent) staffing order.
|
||||||
///
|
///
|
||||||
/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit
|
/// This use case handles urgent, text-based staffing requests and
|
||||||
/// a text-based urgent request via [RapidOrderArguments].
|
/// delegates the submission to the [ClientCreateOrderRepositoryInterface].
|
||||||
class CreateRapidOrderUseCase implements UseCase<RapidOrderArguments, void> {
|
class CreateRapidOrderUseCase implements UseCase<RapidOrderArguments, void> {
|
||||||
|
|
||||||
/// Creates a [CreateRapidOrderUseCase].
|
/// Creates a [CreateRapidOrderUseCase].
|
||||||
|
///
|
||||||
|
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
|
||||||
const CreateRapidOrderUseCase(this._repository);
|
const CreateRapidOrderUseCase(this._repository);
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import '../repositories/client_create_order_repository_interface.dart';
|
|||||||
|
|
||||||
/// Use case for retrieving the available order types for a client.
|
/// Use case for retrieving the available order types for a client.
|
||||||
///
|
///
|
||||||
/// This use case interacts with the [ClientCreateOrderRepositoryInterface] to
|
/// This use case fetches the list of supported staffing order types
|
||||||
/// fetch the list of staffing order types (e.g., Rapid, One-Time).
|
/// from the [ClientCreateOrderRepositoryInterface].
|
||||||
class GetOrderTypesUseCase implements NoInputUseCase<List<OrderType>> {
|
class GetOrderTypesUseCase implements NoInputUseCase<List<OrderType>> {
|
||||||
|
|
||||||
/// Creates a [GetOrderTypesUseCase].
|
/// Creates a [GetOrderTypesUseCase].
|
||||||
|
///
|
||||||
|
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
|
||||||
const GetOrderTypesUseCase(this._repository);
|
const GetOrderTypesUseCase(this._repository);
|
||||||
final ClientCreateOrderRepositoryInterface _repository;
|
final ClientCreateOrderRepositoryInterface _repository;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'client_create_order_state.dart';
|
|||||||
/// BLoC for managing the list of available order types.
|
/// BLoC for managing the list of available order types.
|
||||||
class ClientCreateOrderBloc
|
class ClientCreateOrderBloc
|
||||||
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
|
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
|
||||||
|
|
||||||
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
||||||
: super(const ClientCreateOrderInitial()) {
|
: super(const ClientCreateOrderInitial()) {
|
||||||
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent {
|
class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent {
|
||||||
|
|
||||||
const ClientCreateOrderTypeSelected(this.typeId);
|
const ClientCreateOrderTypeSelected(this.typeId);
|
||||||
final String typeId;
|
final String typeId;
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ class ClientCreateOrderInitial extends ClientCreateOrderState {
|
|||||||
|
|
||||||
/// State representing successfully loaded order types from the repository.
|
/// State representing successfully loaded order types from the repository.
|
||||||
class ClientCreateOrderLoadSuccess extends ClientCreateOrderState {
|
class ClientCreateOrderLoadSuccess extends ClientCreateOrderState {
|
||||||
|
|
||||||
const ClientCreateOrderLoadSuccess(this.orderTypes);
|
const ClientCreateOrderLoadSuccess(this.orderTypes);
|
||||||
|
|
||||||
/// The list of available order types retrieved from the domain.
|
/// The list of available order types retrieved from the domain.
|
||||||
final List<OrderType> orderTypes;
|
final List<OrderType> orderTypes;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'one_time_order_state.dart';
|
|||||||
|
|
||||||
/// BLoC for managing the multi-step one-time order creation form.
|
/// BLoC for managing the multi-step one-time order creation form.
|
||||||
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||||
|
|
||||||
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
|
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
|
||||||
: super(OneTimeOrderState.initial()) {
|
: super(OneTimeOrderState.initial()) {
|
||||||
on<OneTimeOrderDateChanged>(_onDateChanged);
|
on<OneTimeOrderDateChanged>(_onDateChanged);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'rapid_order_state.dart';
|
|||||||
|
|
||||||
/// BLoC for managing the rapid (urgent) order creation flow.
|
/// BLoC for managing the rapid (urgent) order creation flow.
|
||||||
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
||||||
|
|
||||||
RapidOrderBloc(this._createRapidOrderUseCase)
|
RapidOrderBloc(this._createRapidOrderUseCase)
|
||||||
: super(
|
: super(
|
||||||
const RapidOrderInitial(
|
const RapidOrderInitial(
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ abstract class RapidOrderEvent extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RapidOrderMessageChanged extends RapidOrderEvent {
|
class RapidOrderMessageChanged extends RapidOrderEvent {
|
||||||
|
|
||||||
const RapidOrderMessageChanged(this.message);
|
const RapidOrderMessageChanged(this.message);
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ class RapidOrderSubmitted extends RapidOrderEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RapidOrderExampleSelected extends RapidOrderEvent {
|
class RapidOrderExampleSelected extends RapidOrderEvent {
|
||||||
|
|
||||||
const RapidOrderExampleSelected(this.example);
|
const RapidOrderExampleSelected(this.example);
|
||||||
final String example;
|
final String example;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ abstract class RapidOrderState extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RapidOrderInitial extends RapidOrderState {
|
class RapidOrderInitial extends RapidOrderState {
|
||||||
|
|
||||||
const RapidOrderInitial({
|
const RapidOrderInitial({
|
||||||
this.message = '',
|
this.message = '',
|
||||||
this.isListening = false,
|
this.isListening = false,
|
||||||
@@ -43,7 +42,6 @@ class RapidOrderSuccess extends RapidOrderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RapidOrderFailure extends RapidOrderState {
|
class RapidOrderFailure extends RapidOrderState {
|
||||||
|
|
||||||
const RapidOrderFailure(this.error);
|
const RapidOrderFailure(this.error);
|
||||||
final String error;
|
final String error;
|
||||||
|
|
||||||
|
|||||||
@@ -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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.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_bloc.dart';
|
||||||
import '../blocs/client_create_order_event.dart';
|
import '../blocs/client_create_order_event.dart';
|
||||||
import '../blocs/client_create_order_state.dart';
|
import '../widgets/create_order/create_order_view.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main entry page for the client create order flow.
|
/// 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 {
|
class ClientCreateOrderPage extends StatelessWidget {
|
||||||
/// Creates a [ClientCreateOrderPage].
|
/// Creates a [ClientCreateOrderPage].
|
||||||
const ClientCreateOrderPage({super.key});
|
const ClientCreateOrderPage({super.key});
|
||||||
@@ -43,191 +19,7 @@ class ClientCreateOrderPage extends StatelessWidget {
|
|||||||
return BlocProvider<ClientCreateOrderBloc>(
|
return BlocProvider<ClientCreateOrderBloc>(
|
||||||
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
|
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
|
||||||
..add(const ClientCreateOrderTypesRequested()),
|
..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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.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_bloc.dart';
|
||||||
import '../blocs/one_time_order_event.dart';
|
import '../widgets/one_time_order/one_time_order_view.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';
|
|
||||||
|
|
||||||
/// Page for creating a one-time staffing order.
|
/// Page for creating a one-time staffing order.
|
||||||
/// Users can specify the date, location, and multiple staff positions required.
|
/// 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 {
|
class OneTimeOrderPage extends StatelessWidget {
|
||||||
/// Creates a [OneTimeOrderPage].
|
/// Creates a [OneTimeOrderPage].
|
||||||
const OneTimeOrderPage({super.key});
|
const OneTimeOrderPage({super.key});
|
||||||
@@ -23,186 +18,7 @@ class OneTimeOrderPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<OneTimeOrderBloc>(
|
return BlocProvider<OneTimeOrderBloc>(
|
||||||
create: (BuildContext context) => Modular.get<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import '../blocs/rapid_order_bloc.dart';
|
import '../blocs/rapid_order_bloc.dart';
|
||||||
import '../blocs/rapid_order_event.dart';
|
import '../widgets/rapid_order/rapid_order_view.dart';
|
||||||
import '../blocs/rapid_order_state.dart';
|
|
||||||
import '../widgets/rapid_order/rapid_order_example_card.dart';
|
|
||||||
import '../widgets/rapid_order/rapid_order_header.dart';
|
|
||||||
import '../widgets/rapid_order/rapid_order_success_view.dart';
|
|
||||||
|
|
||||||
/// Rapid Order Flow Page - Emergency staffing requests.
|
/// Rapid Order Flow Page - Emergency staffing requests.
|
||||||
/// Features voice recognition simulation and quick example selection.
|
/// 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 {
|
class RapidOrderPage extends StatelessWidget {
|
||||||
/// Creates a [RapidOrderPage].
|
/// Creates a [RapidOrderPage].
|
||||||
const RapidOrderPage({super.key});
|
const RapidOrderPage({super.key});
|
||||||
@@ -21,306 +18,7 @@ class RapidOrderPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<RapidOrderBloc>(
|
return BlocProvider<RapidOrderBloc>(
|
||||||
create: (BuildContext context) => Modular.get<RapidOrderBloc>(),
|
create: (BuildContext context) => Modular.get<RapidOrderBloc>(),
|
||||||
child: const _RapidOrderView(),
|
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,16 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
/// A date picker field for the one-time order form.
|
/// 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.
|
/// The label text to display above the field.
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
@@ -13,56 +22,53 @@ class OneTimeOrderDatePicker extends StatelessWidget {
|
|||||||
/// Callback when a new date is selected.
|
/// Callback when a new date is selected.
|
||||||
final ValueChanged<DateTime> onChanged;
|
final ValueChanged<DateTime> onChanged;
|
||||||
|
|
||||||
/// Creates a [OneTimeOrderDatePicker].
|
@override
|
||||||
const OneTimeOrderDatePicker({
|
State<OneTimeOrderDatePicker> createState() => _OneTimeOrderDatePickerState();
|
||||||
required this.label,
|
}
|
||||||
required this.value,
|
|
||||||
required this.onChanged,
|
class _OneTimeOrderDatePickerState extends State<OneTimeOrderDatePicker> {
|
||||||
super.key,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return UiTextField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
label: widget.label,
|
||||||
children: <Widget>[
|
controller: _controller,
|
||||||
Text(label, style: UiTypography.footnote1m.textSecondary),
|
readOnly: true,
|
||||||
const SizedBox(height: UiConstants.space2),
|
prefixIcon: UiIcons.calendar,
|
||||||
InkWell(
|
onTap: () async {
|
||||||
onTap: () async {
|
final DateTime? picked = await showDatePicker(
|
||||||
final DateTime? picked = await showDatePicker(
|
context: context,
|
||||||
context: context,
|
initialDate: widget.value,
|
||||||
initialDate: value,
|
firstDate: DateTime.now(),
|
||||||
firstDate: DateTime.now(),
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
);
|
||||||
);
|
if (picked != null) {
|
||||||
if (picked != null) {
|
widget.onChanged(picked);
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,8 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// A location input field for the one-time order form.
|
/// A location input field for the one-time order form.
|
||||||
class OneTimeOrderLocationInput extends StatelessWidget {
|
/// Matches the prototype input field style.
|
||||||
/// The label text to display above the field.
|
class OneTimeOrderLocationInput extends StatefulWidget {
|
||||||
final String label;
|
|
||||||
|
|
||||||
/// The current location value.
|
|
||||||
final String value;
|
|
||||||
|
|
||||||
/// Callback when the location text changes.
|
|
||||||
final ValueChanged<String> onChanged;
|
|
||||||
|
|
||||||
/// Creates a [OneTimeOrderLocationInput].
|
/// Creates a [OneTimeOrderLocationInput].
|
||||||
const OneTimeOrderLocationInput({
|
const OneTimeOrderLocationInput({
|
||||||
required this.label,
|
required this.label,
|
||||||
@@ -20,14 +12,50 @@ class OneTimeOrderLocationInput extends StatelessWidget {
|
|||||||
super.key,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return UiTextField(
|
return UiTextField(
|
||||||
label: label,
|
label: widget.label,
|
||||||
hintText: 'Select Branch/Location',
|
controller: _controller,
|
||||||
controller: TextEditingController(text: value)
|
onChanged: widget.onChanged,
|
||||||
..selection = TextSelection.collapsed(offset: value.length),
|
hintText: 'Enter address',
|
||||||
onChanged: onChanged,
|
|
||||||
prefixIcon: UiIcons.mapPin,
|
prefixIcon: UiIcons.mapPin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// A card widget for editing a specific position in a one-time order.
|
/// 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 {
|
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.
|
/// The index of the position in the list.
|
||||||
final int index;
|
final int index;
|
||||||
|
|
||||||
@@ -37,22 +55,6 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
/// Label for the lunch break.
|
/// Label for the lunch break.
|
||||||
final String lunchLabel;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -61,13 +63,6 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
borderRadius: UiConstants.radiusLg,
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
boxShadow: <BoxShadow>[
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -77,149 +72,281 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
'$positionLabel #${index + 1}',
|
'$positionLabel #${index + 1}',
|
||||||
style: UiTypography.body1b.textPrimary,
|
style: UiTypography.footnote1m.textSecondary,
|
||||||
),
|
),
|
||||||
if (isRemovable)
|
if (isRemovable)
|
||||||
IconButton(
|
GestureDetector(
|
||||||
icon: const Icon(UiIcons.delete,
|
onTap: onRemoved,
|
||||||
size: 20, color: UiColors.destructive),
|
child: Text(
|
||||||
onPressed: onRemoved,
|
t.client_create_order.one_time.remove,
|
||||||
padding: EdgeInsets.zero,
|
style: UiTypography.footnote1m.copyWith(
|
||||||
constraints: const BoxConstraints(),
|
color: UiColors.destructive,
|
||||||
visualDensity: VisualDensity.compact,
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
// Role (Dropdown)
|
// Role (Dropdown)
|
||||||
_LabelField(
|
Container(
|
||||||
label: roleLabel,
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
child: DropdownButtonFormField<String>(
|
height: 44,
|
||||||
value: position.role.isEmpty ? null : position.role,
|
decoration: BoxDecoration(
|
||||||
items: <String>['Server', 'Bartender', 'Cook', 'Busser', 'Host']
|
borderRadius: UiConstants.radiusMd,
|
||||||
.map((String role) => DropdownMenuItem<String>(
|
border: Border.all(color: UiColors.border),
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
|
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),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
// Count (Counter)
|
// Optional Location Override
|
||||||
_LabelField(
|
if (position.location == null)
|
||||||
label: workersLabel,
|
GestureDetector(
|
||||||
child: Row(
|
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>[
|
children: <Widget>[
|
||||||
_CounterButton(
|
Row(
|
||||||
icon: UiIcons.minus,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
onPressed: position.count > 1
|
children: <Widget>[
|
||||||
? () => onUpdated(
|
Row(
|
||||||
position.copyWith(count: position.count - 1))
|
children: <Widget>[
|
||||||
: null,
|
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(
|
const SizedBox(height: UiConstants.space2),
|
||||||
padding: const EdgeInsets.symmetric(
|
_PositionLocationInput(
|
||||||
horizontal: UiConstants.space4),
|
value: position.location ?? '',
|
||||||
child: Text('${position.count}',
|
onChanged: (String val) =>
|
||||||
style: UiTypography.headline3m.textPrimary),
|
onUpdated(position.copyWith(location: val)),
|
||||||
),
|
hintText:
|
||||||
_CounterButton(
|
t.client_create_order.one_time.different_location_hint,
|
||||||
icon: UiIcons.add,
|
|
||||||
onPressed: () =>
|
|
||||||
onUpdated(position.copyWith(count: position.count + 1)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
|
|
||||||
// Start/End Time
|
const SizedBox(height: UiConstants.space3),
|
||||||
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),
|
|
||||||
|
|
||||||
// Lunch Break
|
// Lunch Break
|
||||||
_LabelField(
|
Text(
|
||||||
label: lunchLabel,
|
lunchLabel,
|
||||||
child: DropdownButtonFormField<int>(
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
value: position.lunchBreak,
|
),
|
||||||
items: <int>[0, 30, 45, 60]
|
const SizedBox(height: UiConstants.space1),
|
||||||
.map((int mins) => DropdownMenuItem<int>(
|
Container(
|
||||||
value: mins,
|
height: 44,
|
||||||
child: Text('${mins}m',
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
style: UiTypography.body1r.textPrimary),
|
decoration: BoxDecoration(
|
||||||
))
|
borderRadius: UiConstants.radiusMd,
|
||||||
.toList(),
|
border: Border.all(color: UiColors.border),
|
||||||
onChanged: (int? val) {
|
),
|
||||||
if (val != null) {
|
child: DropdownButtonHideUnderline(
|
||||||
onUpdated(position.copyWith(lunchBreak: val));
|
child: DropdownButton<int>(
|
||||||
}
|
isExpanded: true,
|
||||||
},
|
value: position.lunchBreak,
|
||||||
decoration: _inputDecoration(UiIcons.clock),
|
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(
|
Widget _buildTimeInput({
|
||||||
prefixIcon: Icon(icon, size: 18, color: UiColors.iconSecondary),
|
required BuildContext context,
|
||||||
contentPadding:
|
required String label,
|
||||||
const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
required String value,
|
||||||
border: OutlineInputBorder(
|
required VoidCallback onTap,
|
||||||
borderRadius: UiConstants.radiusLg,
|
}) {
|
||||||
borderSide: const BorderSide(color: UiColors.border),
|
return UiTextField(
|
||||||
),
|
label: label,
|
||||||
);
|
controller: TextEditingController(text: value),
|
||||||
|
readOnly: true,
|
||||||
BoxDecoration _boxDecoration() => BoxDecoration(
|
onTap: onTap,
|
||||||
border: Border.all(color: UiColors.border),
|
hintText: '--:--',
|
||||||
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,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
class _PositionLocationInput extends StatefulWidget {
|
||||||
const _CounterButton({required this.icon, this.onPressed});
|
const _PositionLocationInput({
|
||||||
final IconData icon;
|
required this.value,
|
||||||
final VoidCallback? onPressed;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return UiTextField(
|
||||||
onTap: onPressed,
|
controller: _controller,
|
||||||
child: Container(
|
onChanged: widget.onChanged,
|
||||||
width: 32,
|
hintText: widget.hintText,
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
/// A header widget for sections in the one-time order form.
|
/// A header widget for sections in the one-time order form.
|
||||||
class OneTimeOrderSectionHeader extends StatelessWidget {
|
class OneTimeOrderSectionHeader extends StatelessWidget {
|
||||||
|
/// Creates a [OneTimeOrderSectionHeader].
|
||||||
|
const OneTimeOrderSectionHeader({
|
||||||
|
required this.title,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
/// The title text for the section.
|
/// The title text for the section.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@@ -12,14 +20,6 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
|
|||||||
/// Callback when the action button is tapped.
|
/// Callback when the action button is tapped.
|
||||||
final VoidCallback? onAction;
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
/// Creates a [OneTimeOrderSectionHeader].
|
|
||||||
const OneTimeOrderSectionHeader({
|
|
||||||
required this.title,
|
|
||||||
this.actionLabel,
|
|
||||||
this.onAction,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
@@ -27,14 +27,11 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(title, style: UiTypography.headline4m.textPrimary),
|
Text(title, style: UiTypography.headline4m.textPrimary),
|
||||||
if (actionLabel != null && onAction != null)
|
if (actionLabel != null && onAction != null)
|
||||||
TextButton.icon(
|
UiButton.text(
|
||||||
onPressed: onAction,
|
onPressed: onAction,
|
||||||
icon: const Icon(UiIcons.add, size: 16, color: UiColors.primary),
|
leadingIcon: UiIcons.add,
|
||||||
label: Text(actionLabel!, style: UiTypography.body2b.textPrimary),
|
text: actionLabel!,
|
||||||
style: TextButton.styleFrom(
|
iconSize: 16,
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: UiConstants.space2),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,17 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// A view to display when a one-time order has been successfully created.
|
/// 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 {
|
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.
|
/// The title of the success message.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@@ -15,54 +25,78 @@ class OneTimeOrderSuccessView extends StatelessWidget {
|
|||||||
/// Callback when the completion button is tapped.
|
/// Callback when the completion button is tapped.
|
||||||
final VoidCallback onDone;
|
final VoidCallback onDone;
|
||||||
|
|
||||||
/// Creates a [OneTimeOrderSuccessView].
|
|
||||||
const OneTimeOrderSuccessView({
|
|
||||||
required this.title,
|
|
||||||
required this.message,
|
|
||||||
required this.buttonLabel,
|
|
||||||
required this.onDone,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.white,
|
body: Container(
|
||||||
body: Center(
|
width: double.infinity,
|
||||||
child: Padding(
|
decoration: const BoxDecoration(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
|
gradient: LinearGradient(
|
||||||
child: Column(
|
begin: Alignment.topCenter,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
end: Alignment.bottomCenter,
|
||||||
children: <Widget>[
|
colors: <Color>[UiColors.primary, UiColors.buttonPrimaryHover],
|
||||||
Container(
|
),
|
||||||
width: 100,
|
),
|
||||||
height: 100,
|
child: SafeArea(
|
||||||
decoration: const BoxDecoration(
|
child: Center(
|
||||||
color: UiColors.tagSuccess,
|
child: Container(
|
||||||
shape: BoxShape.circle,
|
margin: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
),
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
child: const Icon(UiIcons.check,
|
decoration: BoxDecoration(
|
||||||
size: 50, color: UiColors.textSuccess),
|
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),
|
child: Column(
|
||||||
Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
title,
|
children: <Widget>[
|
||||||
style: UiTypography.headline2m.textPrimary,
|
Container(
|
||||||
textAlign: TextAlign.center,
|
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,21 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
/// A card widget representing an order type in the creation flow.
|
/// A card widget representing an order type in the creation flow.
|
||||||
class OrderTypeCard extends StatelessWidget {
|
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.
|
/// Icon to display at the top of the card.
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
@@ -33,21 +48,6 @@ class OrderTypeCard extends StatelessWidget {
|
|||||||
/// Callback when the card is tapped.
|
/// Callback when the card is tapped.
|
||||||
final VoidCallback onTap;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
/// A card displaying an example message for a rapid order.
|
/// A card displaying an example message for a rapid order.
|
||||||
class RapidOrderExampleCard extends StatelessWidget {
|
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.
|
/// The example text.
|
||||||
final String example;
|
final String example;
|
||||||
|
|
||||||
@@ -15,15 +24,6 @@ class RapidOrderExampleCard extends StatelessWidget {
|
|||||||
/// Callback when the card is tapped.
|
/// Callback when the card is tapped.
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
/// Creates a [RapidOrderExampleCard].
|
|
||||||
const RapidOrderExampleCard({
|
|
||||||
required this.example,
|
|
||||||
required this.isHighlighted,
|
|
||||||
required this.label,
|
|
||||||
required this.onTap,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
/// A header widget for the rapid order flow with a gradient background.
|
/// A header widget for the rapid order flow with a gradient background.
|
||||||
class RapidOrderHeader extends StatelessWidget {
|
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.
|
/// The title of the page.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@@ -18,16 +28,6 @@ class RapidOrderHeader extends StatelessWidget {
|
|||||||
/// Callback when the back button is pressed.
|
/// Callback when the back button is pressed.
|
||||||
final VoidCallback onBack;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
/// A view to display when a rapid order has been successfully created.
|
/// A view to display when a rapid order has been successfully created.
|
||||||
class RapidOrderSuccessView extends StatelessWidget {
|
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.
|
/// The title of the success message.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@@ -15,15 +24,6 @@ class RapidOrderSuccessView extends StatelessWidget {
|
|||||||
/// Callback when the completion button is tapped.
|
/// Callback when the completion button is tapped.
|
||||||
final VoidCallback onDone;
|
final VoidCallback onDone;
|
||||||
|
|
||||||
/// Creates a [RapidOrderSuccessView].
|
|
||||||
const RapidOrderSuccessView({
|
|
||||||
required this.title,
|
|
||||||
required this.message,
|
|
||||||
required this.buttonLabel,
|
|
||||||
required this.onDone,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user