Add order entities and mocks for client order feature

Introduces new domain entities for order types and one-time orders, along with their positions. Adds a mock OrderRepository to the data_connect package and wires it into the module. Updates localization files for new order flows and refactors Equatable usage for consistency. Also adds a minus icon to the design system.
This commit is contained in:
Achintha Isuru
2026-01-22 16:47:39 -05:00
parent 7090efb583
commit 4b3125de1a
80 changed files with 2472 additions and 531 deletions

View File

@@ -1,21 +1,46 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'data/repositories/client_create_order_repository_impl.dart';
import 'domain/repositories/i_client_create_order_repository.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories_impl/client_create_order_repository_impl.dart';
import 'domain/repositories/client_create_order_repository_interface.dart';
import 'domain/usecases/create_one_time_order_usecase.dart';
import 'domain/usecases/create_rapid_order_usecase.dart';
import 'domain/usecases/get_order_types_usecase.dart';
import 'presentation/blocs/client_create_order_bloc.dart';
import 'presentation/blocs/one_time_order_bloc.dart';
import 'presentation/blocs/rapid_order_bloc.dart';
import 'presentation/pages/create_order_page.dart';
import 'presentation/pages/one_time_order_page.dart';
import 'presentation/pages/permanent_order_page.dart';
import 'presentation/pages/rapid_order_page.dart';
import 'presentation/pages/recurring_order_page.dart';
/// Module for the Client Create Order feature.
///
/// This module orchestrates the dependency injection for the create order feature,
/// connecting the domain use cases with their data layer implementations and
/// presentation layer BLoCs.
class ClientCreateOrderModule extends Module {
@override
List<Module> get imports => <Module>[DataConnectModule()];
@override
void binds(Injector i) {
i.add<IClientCreateOrderRepository>(ClientCreateOrderRepositoryImpl.new);
i.add<GetOrderTypesUseCase>(GetOrderTypesUseCase.new);
// Repositories
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
() => ClientCreateOrderRepositoryImpl(
orderMock: i.get<OrderRepositoryMock>()),
);
// UseCases
i.addLazySingleton(GetOrderTypesUseCase.new);
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
i.addLazySingleton(CreateRapidOrderUseCase.new);
// BLoCs
i.addSingleton<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
i.add<RapidOrderBloc>(RapidOrderBloc.new);
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
}
@override

View File

@@ -1,62 +0,0 @@
import 'package:design_system/design_system.dart';
import '../../domain/entities/create_order_type.dart';
import '../../domain/repositories/i_client_create_order_repository.dart';
class ClientCreateOrderRepositoryImpl implements IClientCreateOrderRepository {
@override
Future<List<CreateOrderType>> getOrderTypes() async {
// Simulating async data fetch
await Future.delayed(const Duration(milliseconds: 100));
return [
const CreateOrderType(
id: 'rapid',
icon: UiIcons.zap,
titleKey: 'client_create_order.types.rapid',
descriptionKey: 'client_create_order.types.rapid_desc',
backgroundColor: UiColors.tagError,
borderColor: UiColors.destructive,
iconBackgroundColor: UiColors.tagError,
iconColor: UiColors.destructive,
textColor: UiColors.destructive,
descriptionColor: UiColors.textError,
),
const CreateOrderType(
id: 'one-time',
icon: UiIcons.calendar,
titleKey: 'client_create_order.types.one_time',
descriptionKey: 'client_create_order.types.one_time_desc',
backgroundColor: UiColors.tagInProgress,
borderColor: UiColors.primary,
iconBackgroundColor: UiColors.tagInProgress,
iconColor: UiColors.primary,
textColor: UiColors.primary,
descriptionColor: UiColors.primary,
),
const CreateOrderType(
id: 'recurring',
icon: UiIcons.rotateCcw,
titleKey: 'client_create_order.types.recurring',
descriptionKey: 'client_create_order.types.recurring_desc',
backgroundColor: UiColors.tagRefunded,
borderColor: UiColors.primary,
iconBackgroundColor: UiColors.tagRefunded,
iconColor: UiColors.primary,
textColor: UiColors.primary,
descriptionColor: UiColors.textSecondary,
),
const CreateOrderType(
id: 'permanent',
icon: UiIcons.briefcase,
titleKey: 'client_create_order.types.permanent',
descriptionKey: 'client_create_order.types.permanent_desc',
backgroundColor: UiColors.tagSuccess,
borderColor: UiColors.textSuccess,
iconBackgroundColor: UiColors.tagSuccess,
iconColor: UiColors.textSuccess,
textColor: UiColors.textSuccess,
descriptionColor: UiColors.textSuccess,
),
];
}
}

View File

@@ -0,0 +1,33 @@
import 'package:krow_data_connect/krow_data_connect.dart' hide OrderType;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/client_create_order_repository_interface.dart';
/// Implementation of [ClientCreateOrderRepositoryInterface].
///
/// This implementation delegates all data access to the Data Connect layer,
/// specifically using [OrderRepositoryMock] for now as per the platform's mocking strategy.
class ClientCreateOrderRepositoryImpl
implements ClientCreateOrderRepositoryInterface {
/// Creates a [ClientCreateOrderRepositoryImpl].
///
/// Requires an [OrderRepositoryMock] from the Data Connect shared package.
ClientCreateOrderRepositoryImpl({required OrderRepositoryMock orderMock})
: _orderMock = orderMock;
final OrderRepositoryMock _orderMock;
@override
Future<List<OrderType>> getOrderTypes() {
return _orderMock.getOrderTypes();
}
@override
Future<void> createOneTimeOrder(OneTimeOrder order) {
return _orderMock.createOneTimeOrder(order);
}
@override
Future<void> createRapidOrder(String description) {
return _orderMock.createRapidOrder(description);
}
}

View File

@@ -0,0 +1,13 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
/// Represents the arguments required for the [CreateOneTimeOrderUseCase].
class OneTimeOrderArguments extends UseCaseArgument {
const OneTimeOrderArguments({required this.order});
/// The order details to be created.
final OneTimeOrder order;
@override
List<Object?> get props => <Object?>[order];
}

View File

@@ -0,0 +1,12 @@
import 'package:krow_core/core.dart';
/// Represents the arguments required for the [CreateRapidOrderUseCase].
class RapidOrderArguments extends UseCaseArgument {
const RapidOrderArguments({required this.description});
/// The text description of the urgent staffing need.
final String description;
@override
List<Object?> get props => <Object?>[description];
}

View File

@@ -1,43 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
/// Entity representing an Order Type.
class CreateOrderType extends Equatable {
final String id;
final IconData icon;
final String titleKey; // Key for translation
final String descriptionKey; // Key for translation
final Color backgroundColor;
final Color borderColor;
final Color iconBackgroundColor;
final Color iconColor;
final Color textColor;
final Color descriptionColor;
const CreateOrderType({
required this.id,
required this.icon,
required this.titleKey,
required this.descriptionKey,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
});
@override
List<Object?> get props => [
id,
icon,
titleKey,
descriptionKey,
backgroundColor,
borderColor,
iconBackgroundColor,
iconColor,
textColor,
descriptionColor,
];
}

View File

@@ -0,0 +1,16 @@
import 'package:krow_domain/krow_domain.dart';
/// Interface for the Client Create Order repository.
///
/// This repository handles the retrieval of available order types and the
/// submission of different types of staffing orders (Rapid, One-Time, etc.).
abstract interface class ClientCreateOrderRepositoryInterface {
/// Retrieves the list of available order types.
Future<List<OrderType>> getOrderTypes();
/// Submits a one-time staffing order.
Future<void> createOneTimeOrder(OneTimeOrder order);
/// Submits a rapid (urgent) staffing order with a text description.
Future<void> createRapidOrder(String description);
}

View File

@@ -1,5 +0,0 @@
import '../entities/create_order_type.dart';
abstract interface class IClientCreateOrderRepository {
Future<List<CreateOrderType>> getOrderTypes();
}

View File

@@ -0,0 +1,20 @@
import 'package:krow_core/core.dart';
import '../arguments/one_time_order_arguments.dart';
import '../repositories/client_create_order_repository_interface.dart';
/// Use case for creating a one-time staffing order.
///
/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit
/// a [OneTimeOrder] provided via [OneTimeOrderArguments].
class CreateOneTimeOrderUseCase
implements UseCase<OneTimeOrderArguments, void> {
/// Creates a [CreateOneTimeOrderUseCase].
const CreateOneTimeOrderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;
@override
Future<void> call(OneTimeOrderArguments input) {
return _repository.createOneTimeOrder(input.order);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:krow_core/core.dart';
import '../arguments/rapid_order_arguments.dart';
import '../repositories/client_create_order_repository_interface.dart';
/// Use case for creating a rapid (urgent) staffing order.
///
/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit
/// a text-based urgent request via [RapidOrderArguments].
class CreateRapidOrderUseCase implements UseCase<RapidOrderArguments, void> {
/// Creates a [CreateRapidOrderUseCase].
const CreateRapidOrderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;
@override
Future<void> call(RapidOrderArguments input) {
return _repository.createRapidOrder(input.description);
}
}

View File

@@ -1,12 +1,19 @@
import '../entities/create_order_type.dart';
import '../repositories/i_client_create_order_repository.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/client_create_order_repository_interface.dart';
class GetOrderTypesUseCase {
final IClientCreateOrderRepository _repository;
/// Use case for retrieving the available order types for a client.
///
/// This use case interacts with the [ClientCreateOrderRepositoryInterface] to
/// fetch the list of staffing order types (e.g., Rapid, One-Time).
class GetOrderTypesUseCase implements NoInputUseCase<List<OrderType>> {
GetOrderTypesUseCase(this._repository);
/// Creates a [GetOrderTypesUseCase].
const GetOrderTypesUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;
Future<List<CreateOrderType>> call() {
@override
Future<List<OrderType>> call() {
return _repository.getOrderTypes();
}
}

View File

@@ -1,22 +1,24 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_order_types_usecase.dart';
import 'client_create_order_event.dart';
import 'client_create_order_state.dart';
/// BLoC for managing the list of available order types.
class ClientCreateOrderBloc
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
final GetOrderTypesUseCase _getOrderTypesUseCase;
ClientCreateOrderBloc(this._getOrderTypesUseCase)
: super(const ClientCreateOrderInitial()) {
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
}
final GetOrderTypesUseCase _getOrderTypesUseCase;
Future<void> _onTypesRequested(
ClientCreateOrderTypesRequested event,
Emitter<ClientCreateOrderState> emit,
) async {
final types = await _getOrderTypesUseCase();
final List<OrderType> types = await _getOrderTypesUseCase();
emit(ClientCreateOrderLoadSuccess(types));
}
}

View File

@@ -4,7 +4,7 @@ abstract class ClientCreateOrderEvent extends Equatable {
const ClientCreateOrderEvent();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent {
@@ -12,10 +12,10 @@ class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent {
}
class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent {
final String typeId;
const ClientCreateOrderTypeSelected(this.typeId);
final String typeId;
@override
List<Object?> get props => [typeId];
List<Object?> get props => <Object?>[typeId];
}

View File

@@ -1,23 +1,26 @@
import 'package:equatable/equatable.dart';
import '../../domain/entities/create_order_type.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
/// Base state for the [ClientCreateOrderBloc].
abstract class ClientCreateOrderState extends Equatable {
const ClientCreateOrderState();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
/// Initial state when order types haven't been loaded yet.
class ClientCreateOrderInitial extends ClientCreateOrderState {
const ClientCreateOrderInitial();
}
/// State representing successfully loaded order types from the repository.
class ClientCreateOrderLoadSuccess extends ClientCreateOrderState {
final List<CreateOrderType> orderTypes;
const ClientCreateOrderLoadSuccess(this.orderTypes);
/// The list of available order types retrieved from the domain.
final List<OrderType> orderTypes;
@override
List<Object?> get props => [orderTypes];
List<Object?> get props => <Object?>[orderTypes];
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/one_time_order_arguments.dart';
import '../../domain/usecases/create_one_time_order_usecase.dart';
import 'one_time_order_event.dart';
import 'one_time_order_state.dart';
/// BLoC for managing the multi-step one-time order creation form.
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
: super(OneTimeOrderState.initial()) {
on<OneTimeOrderDateChanged>(_onDateChanged);
on<OneTimeOrderLocationChanged>(_onLocationChanged);
on<OneTimeOrderPositionAdded>(_onPositionAdded);
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
on<OneTimeOrderSubmitted>(_onSubmitted);
}
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
void _onDateChanged(
OneTimeOrderDateChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(state.copyWith(date: event.date));
}
void _onLocationChanged(
OneTimeOrderLocationChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(state.copyWith(location: event.location));
}
void _onPositionAdded(
OneTimeOrderPositionAdded event,
Emitter<OneTimeOrderState> emit,
) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions)
..add(const OneTimeOrderPosition(
role: '',
count: 1,
startTime: '',
endTime: '',
));
emit(state.copyWith(positions: newPositions));
}
void _onPositionRemoved(
OneTimeOrderPositionRemoved event,
Emitter<OneTimeOrderState> emit,
) {
if (state.positions.length > 1) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions)
..removeAt(event.index);
emit(state.copyWith(positions: newPositions));
}
}
void _onPositionUpdated(
OneTimeOrderPositionUpdated event,
Emitter<OneTimeOrderState> emit,
) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions);
newPositions[event.index] = event.position;
emit(state.copyWith(positions: newPositions));
}
Future<void> _onSubmitted(
OneTimeOrderSubmitted event,
Emitter<OneTimeOrderState> emit,
) async {
emit(state.copyWith(status: OneTimeOrderStatus.loading));
try {
final OneTimeOrder order = OneTimeOrder(
date: state.date,
location: state.location,
positions: state.positions,
);
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
emit(state.copyWith(status: OneTimeOrderStatus.success));
} catch (e) {
emit(state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: e.toString(),
));
}
}
}

View File

@@ -0,0 +1,50 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class OneTimeOrderEvent extends Equatable {
const OneTimeOrderEvent();
@override
List<Object?> get props => <Object?>[];
}
class OneTimeOrderDateChanged extends OneTimeOrderEvent {
const OneTimeOrderDateChanged(this.date);
final DateTime date;
@override
List<Object?> get props => <Object?>[date];
}
class OneTimeOrderLocationChanged extends OneTimeOrderEvent {
const OneTimeOrderLocationChanged(this.location);
final String location;
@override
List<Object?> get props => <Object?>[location];
}
class OneTimeOrderPositionAdded extends OneTimeOrderEvent {
const OneTimeOrderPositionAdded();
}
class OneTimeOrderPositionRemoved extends OneTimeOrderEvent {
const OneTimeOrderPositionRemoved(this.index);
final int index;
@override
List<Object?> get props => <Object?>[index];
}
class OneTimeOrderPositionUpdated extends OneTimeOrderEvent {
const OneTimeOrderPositionUpdated(this.index, this.position);
final int index;
final OneTimeOrderPosition position;
@override
List<Object?> get props => <Object?>[index, position];
}
class OneTimeOrderSubmitted extends OneTimeOrderEvent {
const OneTimeOrderSubmitted();
}

View File

@@ -0,0 +1,59 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
enum OneTimeOrderStatus { initial, loading, success, failure }
class OneTimeOrderState extends Equatable {
const OneTimeOrderState({
required this.date,
required this.location,
required this.positions,
this.status = OneTimeOrderStatus.initial,
this.errorMessage,
});
factory OneTimeOrderState.initial() {
return OneTimeOrderState(
date: DateTime.now(),
location: '',
positions: const <OneTimeOrderPosition>[
OneTimeOrderPosition(
role: '',
count: 1,
startTime: '',
endTime: '',
),
],
);
}
final DateTime date;
final String location;
final List<OneTimeOrderPosition> positions;
final OneTimeOrderStatus status;
final String? errorMessage;
OneTimeOrderState copyWith({
DateTime? date,
String? location,
List<OneTimeOrderPosition>? positions,
OneTimeOrderStatus? status,
String? errorMessage,
}) {
return OneTimeOrderState(
date: date ?? this.date,
location: location ?? this.location,
positions: positions ?? this.positions,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => <Object?>[
date,
location,
positions,
status,
errorMessage,
];
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/arguments/rapid_order_arguments.dart';
import '../../domain/usecases/create_rapid_order_usecase.dart';
import 'rapid_order_event.dart';
import 'rapid_order_state.dart';
/// BLoC for managing the rapid (urgent) order creation flow.
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
RapidOrderBloc(this._createRapidOrderUseCase)
: super(
const RapidOrderInitial(
examples: <String>[
'"We had a call out. Need 2 cooks ASAP"',
'"Need 5 bartenders ASAP until 5am"',
'"Emergency! Need 3 servers right now till midnight"',
],
),
) {
on<RapidOrderMessageChanged>(_onMessageChanged);
on<RapidOrderVoiceToggled>(_onVoiceToggled);
on<RapidOrderSubmitted>(_onSubmitted);
on<RapidOrderExampleSelected>(_onExampleSelected);
}
final CreateRapidOrderUseCase _createRapidOrderUseCase;
void _onMessageChanged(
RapidOrderMessageChanged event,
Emitter<RapidOrderState> emit,
) {
if (state is RapidOrderInitial) {
emit((state as RapidOrderInitial).copyWith(message: event.message));
}
}
Future<void> _onVoiceToggled(
RapidOrderVoiceToggled event,
Emitter<RapidOrderState> emit,
) async {
if (state is RapidOrderInitial) {
final RapidOrderInitial currentState = state as RapidOrderInitial;
final bool newListeningState = !currentState.isListening;
emit(currentState.copyWith(isListening: newListeningState));
// Simulate voice recognition
if (newListeningState) {
await Future.delayed(const Duration(seconds: 2));
if (state is RapidOrderInitial) {
emit(
(state as RapidOrderInitial).copyWith(
message: 'Need 2 servers for a banquet right now.',
isListening: false,
),
);
}
}
}
}
Future<void> _onSubmitted(
RapidOrderSubmitted event,
Emitter<RapidOrderState> emit,
) async {
final RapidOrderState currentState = state;
if (currentState is RapidOrderInitial) {
final String message = currentState.message;
emit(const RapidOrderSubmitting());
try {
await _createRapidOrderUseCase(
RapidOrderArguments(description: message));
emit(const RapidOrderSuccess());
} catch (e) {
emit(RapidOrderFailure(e.toString()));
}
}
}
void _onExampleSelected(
RapidOrderExampleSelected event,
Emitter<RapidOrderState> emit,
) {
if (state is RapidOrderInitial) {
final String cleanedExample = event.example.replaceAll('"', '');
emit((state as RapidOrderInitial).copyWith(message: cleanedExample));
}
}
}

View File

@@ -0,0 +1,34 @@
import 'package:equatable/equatable.dart';
abstract class RapidOrderEvent extends Equatable {
const RapidOrderEvent();
@override
List<Object?> get props => <Object?>[];
}
class RapidOrderMessageChanged extends RapidOrderEvent {
const RapidOrderMessageChanged(this.message);
final String message;
@override
List<Object?> get props => <Object?>[message];
}
class RapidOrderVoiceToggled extends RapidOrderEvent {
const RapidOrderVoiceToggled();
}
class RapidOrderSubmitted extends RapidOrderEvent {
const RapidOrderSubmitted();
}
class RapidOrderExampleSelected extends RapidOrderEvent {
const RapidOrderExampleSelected(this.example);
final String example;
@override
List<Object?> get props => <Object?>[example];
}

View File

@@ -0,0 +1,52 @@
import 'package:equatable/equatable.dart';
abstract class RapidOrderState extends Equatable {
const RapidOrderState();
@override
List<Object?> get props => <Object?>[];
}
class RapidOrderInitial extends RapidOrderState {
const RapidOrderInitial({
this.message = '',
this.isListening = false,
required this.examples,
});
final String message;
final bool isListening;
final List<String> examples;
@override
List<Object?> get props => <Object?>[message, isListening, examples];
RapidOrderInitial copyWith({
String? message,
bool? isListening,
List<String>? examples,
}) {
return RapidOrderInitial(
message: message ?? this.message,
isListening: isListening ?? this.isListening,
examples: examples ?? this.examples,
);
}
}
class RapidOrderSubmitting extends RapidOrderState {
const RapidOrderSubmitting();
}
class RapidOrderSuccess extends RapidOrderState {
const RapidOrderSuccess();
}
class RapidOrderFailure extends RapidOrderState {
const RapidOrderFailure(this.error);
final String error;
@override
List<Object?> get props => <Object?>[error];
}

View File

@@ -3,7 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../domain/entities/create_order_type.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/client_create_order_bloc.dart';
import '../blocs/client_create_order_event.dart';
import '../blocs/client_create_order_state.dart';
@@ -11,7 +11,6 @@ import '../navigation/client_create_order_navigator.dart';
/// One-time helper to map keys to translations since they are dynamic in BLoC state
String _getTranslation(String key) {
// Safe mapping - explicit keys expected
if (key == 'client_create_order.types.rapid') {
return t.client_create_order.types.rapid;
} else if (key == 'client_create_order.types.rapid_desc') {
@@ -76,7 +75,7 @@ class _CreateOrderView extends StatelessWidget {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space6),
child: Text(
@@ -103,19 +102,22 @@ class _CreateOrderView extends StatelessWidget {
),
itemCount: state.orderTypes.length,
itemBuilder: (BuildContext context, int index) {
final CreateOrderType type = state.orderTypes[index];
final OrderType type = state.orderTypes[index];
final _OrderTypeUiMetadata ui =
_OrderTypeUiMetadata.fromId(type.id);
return _OrderTypeCard(
icon: type.icon,
icon: ui.icon,
title: _getTranslation(type.titleKey),
description: _getTranslation(
type.descriptionKey,
),
backgroundColor: type.backgroundColor,
borderColor: type.borderColor,
iconBackgroundColor: type.iconBackgroundColor,
iconColor: type.iconColor,
textColor: type.textColor,
descriptionColor: type.descriptionColor,
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.descriptionColor,
onTap: () {
switch (type.id) {
case 'rapid':
@@ -149,17 +151,6 @@ class _CreateOrderView extends StatelessWidget {
}
class _OrderTypeCard extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final Color backgroundColor;
final Color borderColor;
final Color iconBackgroundColor;
final Color iconColor;
final Color textColor;
final Color descriptionColor;
final VoidCallback onTap;
const _OrderTypeCard({
required this.icon,
required this.title,
@@ -173,6 +164,17 @@ class _OrderTypeCard extends StatelessWidget {
required this.onTap,
});
final IconData icon;
final String title;
final String description;
final Color backgroundColor;
final Color borderColor;
final Color iconBackgroundColor;
final Color iconColor;
final Color textColor;
final Color descriptionColor;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -187,7 +189,7 @@ class _OrderTypeCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
children: <Widget>[
Container(
width: 48,
height: 48,
@@ -213,3 +215,78 @@ class _OrderTypeCard extends StatelessWidget {
);
}
}
class _OrderTypeUiMetadata {
const _OrderTypeUiMetadata({
required this.icon,
required this.backgroundColor,
required this.borderColor,
required this.iconBackgroundColor,
required this.iconColor,
required this.textColor,
required this.descriptionColor,
});
factory _OrderTypeUiMetadata.fromId(String id) {
switch (id) {
case 'rapid':
return const _OrderTypeUiMetadata(
icon: UiIcons.zap,
backgroundColor: Color(0xFFFFF7ED),
borderColor: Color(0xFFFFEDD5),
iconBackgroundColor: Color(0xFFF97316),
iconColor: Colors.white,
textColor: Color(0xFF9A3412),
descriptionColor: Color(0xFFC2410C),
);
case 'one-time':
return const _OrderTypeUiMetadata(
icon: UiIcons.calendar,
backgroundColor: Color(0xFFF0F9FF),
borderColor: Color(0xFFE0F2FE),
iconBackgroundColor: Color(0xFF0EA5E9),
iconColor: Colors.white,
textColor: Color(0xFF075985),
descriptionColor: Color(0xFF0369A1),
);
case 'recurring':
return const _OrderTypeUiMetadata(
icon: UiIcons.rotateCcw,
backgroundColor: Color(0xFFF0FDF4),
borderColor: Color(0xFFDCFCE7),
iconBackgroundColor: Color(0xFF22C55E),
iconColor: Colors.white,
textColor: Color(0xFF166534),
descriptionColor: Color(0xFF15803D),
);
case 'permanent':
return const _OrderTypeUiMetadata(
icon: UiIcons.briefcase,
backgroundColor: Color(0xFFF5F3FF),
borderColor: Color(0xFFEDE9FE),
iconBackgroundColor: Color(0xFF8B5CF6),
iconColor: Colors.white,
textColor: Color(0xFF5B21B6),
descriptionColor: Color(0xFF6D28D9),
);
default:
return const _OrderTypeUiMetadata(
icon: UiIcons.help,
backgroundColor: Colors.grey,
borderColor: Colors.grey,
iconBackgroundColor: Colors.grey,
iconColor: Colors.white,
textColor: Colors.black,
descriptionColor: Colors.black54,
);
}
}
final IconData icon;
final Color backgroundColor;
final Color borderColor;
final Color iconBackgroundColor;
final Color iconColor;
final Color textColor;
final Color descriptionColor;
}

View File

@@ -1,31 +1,648 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/one_time_order_bloc.dart';
import '../blocs/one_time_order_event.dart';
import '../blocs/one_time_order_state.dart';
/// One-Time Order Page - Single event or shift request
class OneTimeOrderPage extends StatelessWidget {
const OneTimeOrderPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text('One-Time Order', style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
return BlocProvider<OneTimeOrderBloc>(
create: (BuildContext context) => Modular.get<OneTimeOrderBloc>(),
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 const _SuccessView();
}
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text(labels.title, style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft,
color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
),
body: Stack(
children: <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>[
_SectionHeader(title: labels.create_your_order),
const SizedBox(height: UiConstants.space4),
// Date Picker Field
_DatePickerField(
label: labels.date_label,
value: state.date,
onChanged: (DateTime date) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderDateChanged(date)),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
const SizedBox(height: UiConstants.space4),
// Location Field
_LocationField(
label: labels.location_label,
value: state.location,
onChanged: (String location) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderLocationChanged(location)),
),
const SizedBox(height: UiConstants.space6),
_SectionHeader(
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: _PositionCard(
index: index,
position: position,
isRemovable: state.positions.length > 1,
),
);
}),
const SizedBox(height: 100), // Space for bottom button
],
);
}
}
class _SectionHeader extends StatelessWidget {
const _SectionHeader({
required this.title,
this.actionLabel,
this.onAction,
});
final String title;
final String? actionLabel;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(title, style: UiTypography.headline4m.textPrimary),
if (actionLabel != null && onAction != null)
TextButton.icon(
onPressed: onAction,
icon: const Icon(UiIcons.add, size: 16, color: UiColors.primary),
label: Text(actionLabel!, style: UiTypography.body2b.textPrimary),
style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: UiConstants.space2),
),
),
],
);
}
}
class _DatePickerField extends StatelessWidget {
const _DatePickerField({
required this.label,
required this.value,
required this.onChanged,
});
final String label;
final DateTime value;
final ValueChanged<DateTime> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space2),
InkWell(
onTap: () async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: value,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (picked != null) onChanged(picked);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3 + 2,
),
decoration: BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
),
child: Row(
children: <Widget>[
const Icon(UiIcons.calendar,
size: 20, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space3),
Text(
DateFormat('EEEE, MMM d, yyyy').format(value),
style: UiTypography.body1r.textPrimary,
),
],
),
),
),
],
);
}
}
class _LocationField extends StatelessWidget {
const _LocationField({
required this.label,
required this.value,
required this.onChanged,
});
final String label;
final String value;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space2),
// Simplified for now - can use a dropdown or Autocomplete
TextField(
controller: TextEditingController(text: value)
..selection = TextSelection.collapsed(offset: value.length),
onChanged: onChanged,
decoration: InputDecoration(
hintText: 'Select Branch/Location',
prefixIcon: const Icon(UiIcons.mapPin,
size: 20, color: UiColors.iconSecondary),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(color: UiColors.border),
),
),
style: UiTypography.body1r.textPrimary,
),
],
);
}
}
class _PositionCard extends StatelessWidget {
const _PositionCard({
required this.index,
required this.position,
required this.isRemovable,
});
final int index;
final OneTimeOrderPosition position;
final bool isRemovable;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
body: Center(
child: Text('One-Time Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'${labels.positions_title} #${index + 1}',
style: UiTypography.body1b.textPrimary,
),
if (isRemovable)
IconButton(
icon: const Icon(UiIcons.delete,
size: 20, color: UiColors.destructive),
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderPositionRemoved(index)),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
),
],
),
const Divider(height: UiConstants.space6),
// Role (Dropdown simulation)
_LabelField(
label: labels.select_role,
child: DropdownButtonFormField<String>(
initialValue: position.role.isEmpty ? null : position.role,
items: <String>['Server', 'Bartender', 'Cook', 'Busser', 'Host']
.map((String role) => DropdownMenuItem<String>(
value: role,
child:
Text(role, style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (String? val) {
if (val != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index, position.copyWith(role: val)),
);
}
},
decoration: _inputDecoration(UiIcons.briefcase),
),
),
const SizedBox(height: UiConstants.space4),
// Count
_LabelField(
label: labels.workers_label,
child: Row(
children: <Widget>[
_CounterButton(
icon: UiIcons.minus,
onPressed: position.count > 1
? () => BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(index,
position.copyWith(count: position.count - 1)),
)
: null,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4),
child: Text('${position.count}',
style: UiTypography.headline3m.textPrimary),
),
_CounterButton(
icon: UiIcons.add,
onPressed: () =>
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index, position.copyWith(count: position.count + 1)),
),
),
],
),
),
const SizedBox(height: UiConstants.space4),
// Start/End Time
Row(
children: <Widget>[
Expanded(
child: _LabelField(
label: labels.start_label,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 9, minute: 0),
);
if (picked != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index,
position.copyWith(
startTime: picked.format(context)),
),
);
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.startTime.isEmpty
? '--:--'
: position.startTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _LabelField(
label: labels.end_label,
child: InkWell(
onTap: () async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 17, minute: 0),
);
if (picked != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index,
position.copyWith(endTime: picked.format(context)),
),
);
}
},
child: Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: _boxDecoration(),
child: Text(
position.endTime.isEmpty ? '--:--' : position.endTime,
style: UiTypography.body1r.textPrimary,
),
),
),
),
),
],
),
const SizedBox(height: UiConstants.space4),
// Lunch Break
_LabelField(
label: labels.lunch_break_label,
child: DropdownButtonFormField<int>(
initialValue: position.lunchBreak,
items: <int>[0, 30, 45, 60]
.map((int mins) => DropdownMenuItem<int>(
value: mins,
child: Text('${mins}m',
style: UiTypography.body1r.textPrimary),
))
.toList(),
onChanged: (int? val) {
if (val != null) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(
index, position.copyWith(lunchBreak: val)),
);
}
},
decoration: _inputDecoration(UiIcons.clock),
),
),
],
),
);
}
InputDecoration _inputDecoration(IconData icon) => InputDecoration(
prefixIcon: Icon(icon, size: 18, color: UiColors.iconSecondary),
contentPadding:
const EdgeInsets.symmetric(horizontal: UiConstants.space3),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusLg,
borderSide: const BorderSide(color: UiColors.border),
),
);
BoxDecoration _boxDecoration() => BoxDecoration(
border: Border.all(color: UiColors.border),
borderRadius: UiConstants.radiusLg,
);
}
class _LabelField extends StatelessWidget {
const _LabelField({required this.label, required this.child});
final String label;
final Widget child;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote1m.textSecondary),
const SizedBox(height: UiConstants.space1),
child,
],
);
}
}
class _CounterButton extends StatelessWidget {
const _CounterButton({required this.icon, this.onPressed});
final IconData icon;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border.all(
color: onPressed != null
? UiColors.border
: UiColors.border.withOpacity(0.5)),
borderRadius: UiConstants.radiusLg,
color: onPressed != null ? UiColors.white : UiColors.background,
),
child: Icon(
icon,
size: 16,
color: onPressed != null
? UiColors.iconPrimary
: UiColors.iconSecondary.withOpacity(0.5),
),
),
);
}
}
class _BottomActionButton extends StatelessWidget {
const _BottomActionButton({
required this.label,
required this.onPressed,
this.isLoading = false,
});
final String label;
final VoidCallback onPressed;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: UiConstants.space5,
right: UiConstants.space5,
top: UiConstants.space4,
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space4,
),
decoration: BoxDecoration(
color: UiColors.white,
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -4),
),
],
),
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: UiColors.white,
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusLg,
),
elevation: 0,
),
child: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: UiColors.white, strokeWidth: 2),
)
: Text(label,
style: UiTypography.body1b.copyWith(color: UiColors.white)),
),
);
}
}
class _SuccessView extends StatelessWidget {
const _SuccessView();
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return Scaffold(
backgroundColor: UiColors.white,
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
color: UiColors.tagSuccess,
shape: BoxShape.circle,
),
child: const Icon(UiIcons.check,
size: 50, color: UiColors.textSuccess),
),
const SizedBox(height: UiConstants.space8),
Text(
labels.success_title,
style: UiTypography.headline2m.textPrimary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space4),
Text(
labels.success_message,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
const SizedBox(height: UiConstants.space10),
ElevatedButton(
onPressed: () => Modular.to.pop(),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: UiColors.white,
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusLg,
),
),
child: Text('Done',
style: UiTypography.body1b.copyWith(color: UiColors.white)),
),
],
),
),
),
);
}

View File

@@ -1,17 +1,21 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
/// Permanent Order Page - Long-term staffing placement
class PermanentOrderPage extends StatelessWidget {
const PermanentOrderPage({super.key});
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderPermanentEn labels =
t.client_create_order.permanent;
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text('Permanent Order', style: UiTypography.headline3m.textPrimary),
title: Text(labels.title, style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
@@ -24,8 +28,16 @@ class PermanentOrderPage extends StatelessWidget {
),
),
body: Center(
child: Text('Permanent Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
labels.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
);
}

View File

@@ -1,30 +1,561 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import '../blocs/rapid_order_bloc.dart';
import '../blocs/rapid_order_event.dart';
import '../blocs/rapid_order_state.dart';
/// Rapid Order Flow Page - Emergency staffing requests
class RapidOrderPage extends StatelessWidget {
const RapidOrderPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title: Text('Rapid Order', style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
return BlocProvider<RapidOrderBloc>(
create: (BuildContext context) => Modular.get<RapidOrderBloc>(),
child: const _RapidOrderView(),
);
}
}
class _RapidOrderView extends StatelessWidget {
const _RapidOrderView();
@override
Widget build(BuildContext context) {
return BlocBuilder<RapidOrderBloc, RapidOrderState>(
builder: (BuildContext context, RapidOrderState state) {
if (state is RapidOrderSuccess) {
return const _SuccessView();
}
return const _RapidOrderForm();
},
);
}
}
class _RapidOrderForm extends StatefulWidget {
const _RapidOrderForm();
@override
State<_RapidOrderForm> createState() => _RapidOrderFormState();
}
class _RapidOrderFormState extends State<_RapidOrderForm> {
final TextEditingController _messageController = TextEditingController();
@override
void dispose() {
_messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels = t.client_create_order.rapid;
final DateTime now = DateTime.now();
final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now);
final String timeStr = DateFormat('h:mm a').format(now);
return BlocListener<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.background,
body: Column(
children: <Widget>[
// Header with gradient
Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + UiConstants.space5,
bottom: UiConstants.space5,
left: UiConstants.space5,
right: UiConstants.space5,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.destructive,
UiColors.destructive.withValues(alpha: 0.85),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.pop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
const Icon(
UiIcons.zap,
color: UiColors.accent,
size: 18,
),
const SizedBox(width: UiConstants.space2),
Text(
labels.title,
style: UiTypography.headline3m.copyWith(
color: UiColors.white,
),
),
],
),
Text(
labels.subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
dateStr,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
Text(
timeStr,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.9),
),
),
],
),
],
),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <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
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,
),
),
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 isFirst = index == 0;
return Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space2),
child: GestureDetector(
onTap: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
RapidOrderExampleSelected(example),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
vertical: UiConstants.space3,
),
decoration: BoxDecoration(
color: isFirst
? UiColors.accent
.withValues(alpha: 0.15)
: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(
color: isFirst
? UiColors.accent
: UiColors.border,
),
),
child: RichText(
text: TextSpan(
style:
UiTypography.body2r.textPrimary,
children: <InlineSpan>[
TextSpan(
text: labels.example,
style: UiTypography
.body2b.textPrimary,
),
TextSpan(text: example),
],
),
),
),
),
);
}),
const SizedBox(height: UiConstants.space4),
// Input
TextField(
controller: _messageController,
maxLines: 4,
onChanged: (String value) {
BlocProvider.of<RapidOrderBloc>(context).add(
RapidOrderMessageChanged(value),
);
},
decoration: InputDecoration(
hintText: labels.hint,
hintStyle: UiTypography.body2r.copyWith(
color: UiColors.textPlaceholder,
),
border: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderSide: const BorderSide(
color: UiColors.border,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderSide: const BorderSide(
color: UiColors.border,
),
),
contentPadding:
const EdgeInsets.all(UiConstants.space4),
),
),
const SizedBox(height: UiConstants.space4),
// Actions
Row(
children: <Widget>[
Expanded(
child: SizedBox(
height: 52,
child: OutlinedButton.icon(
onPressed: initialState != null
? () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
const RapidOrderVoiceToggled(),
)
: null,
icon: Icon(
UiIcons
.bell, // Using bell as mic placeholder
size: 20,
color:
initialState?.isListening == true
? UiColors.destructive
: UiColors.iconPrimary,
),
label: Text(
initialState?.isListening == true
? labels.listening
: labels.speak,
style: UiTypography.body2b.copyWith(
color: initialState?.isListening ==
true
? UiColors.destructive
: UiColors.textPrimary,
),
),
style: OutlinedButton.styleFrom(
backgroundColor:
initialState?.isListening == true
? UiColors.destructive
.withValues(alpha: 0.05)
: UiColors.white,
side: BorderSide(
color: initialState?.isListening ==
true
? UiColors.destructive
: UiColors.border,
),
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
),
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: SizedBox(
height: 52,
child: ElevatedButton.icon(
onPressed: isSubmitting ||
(initialState?.message
.trim()
.isEmpty ??
true)
? null
: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
const RapidOrderSubmitted(),
),
icon: const Icon(
UiIcons.arrowRight,
size: 20,
color: UiColors.white,
),
label: Text(
isSubmitting
? labels.sending
: labels.send,
style: UiTypography.body2b.copyWith(
color: UiColors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
elevation: 0,
),
),
),
),
],
),
],
);
},
),
),
],
),
),
),
],
),
),
);
}
}
class _SuccessView extends StatelessWidget {
const _SuccessView();
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRapidEn labels = t.client_create_order.rapid;
return Scaffold(
body: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
UiColors.primary,
UiColors.primary.withValues(alpha: 0.85),
],
),
),
child: SafeArea(
child: Center(
child: Container(
margin:
const EdgeInsets.symmetric(horizontal: UiConstants.space10),
padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
color: UiColors.accent,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
UiIcons.zap,
color: UiColors.textPrimary,
size: 32,
),
),
),
const SizedBox(height: UiConstants.space6),
Text(
labels.success_title,
style: UiTypography.headline1m.textPrimary,
),
const SizedBox(height: UiConstants.space3),
Text(
labels.success_message,
textAlign: TextAlign.center,
style: UiTypography.body2r.copyWith(
color: UiColors.textSecondary,
height: 1.5,
),
),
const SizedBox(height: UiConstants.space8),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: () => Modular.to.pop(),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.textPrimary,
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusMd,
),
elevation: 0,
),
child: Text(
labels.back_to_orders,
style: UiTypography.body1b.copyWith(
color: UiColors.white,
),
),
),
),
],
),
),
),
),
backgroundColor: UiColors.white,
elevation: 0,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
),
body: Center(
child: Text('Rapid Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
),
);
}

View File

@@ -1,17 +1,21 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
/// Recurring Order Page - Ongoing weekly/monthly coverage
class RecurringOrderPage extends StatelessWidget {
const RecurringOrderPage({super.key});
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRecurringEn labels =
t.client_create_order.recurring;
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
title:
Text('Recurring Order', style: UiTypography.headline3m.textPrimary),
title: Text(labels.title, style: UiTypography.headline3m.textPrimary),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
onPressed: () => Modular.to.pop(),
@@ -24,8 +28,16 @@ class RecurringOrderPage extends StatelessWidget {
),
),
body: Center(
child: Text('Recurring Order Flow (WIP)',
style: UiTypography.body1r.textSecondary),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
labels.subtitle,
style: UiTypography.body1r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
);
}

View File

@@ -300,7 +300,7 @@ packages:
source: hosted
version: "4.1.2"
intl:
dependency: transitive
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
@@ -324,12 +324,19 @@ packages:
source: hosted
version: "0.7.2"
krow_core:
dependency: transitive
dependency: "direct main"
description:
path: "../../../core"
relative: true
source: path
version: "0.0.1"
krow_data_connect:
dependency: "direct main"
description:
path: "../../../data_connect"
relative: true
source: path
version: "0.0.1"
krow_domain:
dependency: "direct main"
description:

View File

@@ -12,12 +12,17 @@ dependencies:
flutter_bloc: ^8.1.3
flutter_modular: ^6.3.2
equatable: ^2.0.5
intl: 0.20.2
design_system:
path: ../../../design_system
core_localization:
path: ../../../core_localization
krow_domain:
path: ../../../domain
krow_core:
path: ../../../core
krow_data_connect:
path: ../../../data_connect
dev_dependencies:
flutter_test: