Merge branch '208-p0-auth-05-get-started-screen' of github.com:Oloodi/krow-workforce into 208-p0-auth-05-get-started-screen
This commit is contained in:
@@ -1,16 +1,46 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_modular/flutter_modular.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) {
|
||||
// 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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for retrieving the available order types for a client.
|
||||
///
|
||||
/// This use case interacts with the [ClientCreateOrderRepositoryInterface] to
|
||||
/// fetch the list of staffing order types (e.g., Rapid, One-Time).
|
||||
class GetOrderTypesUseCase implements NoInputUseCase<List<OrderType>> {
|
||||
|
||||
/// Creates a [GetOrderTypesUseCase].
|
||||
const GetOrderTypesUseCase(this._repository);
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<OrderType>> call() {
|
||||
return _repository.getOrderTypes();
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,24 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:design_system/design_system.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> {
|
||||
ClientCreateOrderBloc() : super(const ClientCreateOrderInitial()) {
|
||||
|
||||
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
||||
: super(const ClientCreateOrderInitial()) {
|
||||
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
||||
}
|
||||
final GetOrderTypesUseCase _getOrderTypesUseCase;
|
||||
|
||||
void _onTypesRequested(
|
||||
Future<void> _onTypesRequested(
|
||||
ClientCreateOrderTypesRequested event,
|
||||
Emitter<ClientCreateOrderState> emit,
|
||||
) {
|
||||
// In a real app, this might come from a repository or config
|
||||
final List<CreateOrderType> types = [
|
||||
const CreateOrderType(
|
||||
id: 'rapid',
|
||||
icon: UiIcons.zap,
|
||||
titleKey: 'client_create_order.types.rapid',
|
||||
descriptionKey: 'client_create_order.types.rapid_desc',
|
||||
backgroundColor: UiColors.tagError, // Red-ish background
|
||||
borderColor: UiColors.destructive, // Red border
|
||||
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, // Blue-ish
|
||||
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, // Indigo-ish (Purple sub)
|
||||
borderColor: UiColors.primary, // No purple, use primary or mix
|
||||
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, // Green
|
||||
borderColor: UiColors.textSuccess,
|
||||
iconBackgroundColor: UiColors.tagSuccess,
|
||||
iconColor: UiColors.textSuccess,
|
||||
textColor: UiColors.textSuccess,
|
||||
descriptionColor: UiColors.textSuccess,
|
||||
),
|
||||
];
|
||||
) async {
|
||||
final List<OrderType> types = await _getOrderTypesUseCase();
|
||||
emit(ClientCreateOrderLoadSuccess(types));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -1,63 +1,26 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Represents an available 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,
|
||||
];
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -3,14 +3,15 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/client_create_order_bloc.dart';
|
||||
import '../blocs/client_create_order_event.dart';
|
||||
import '../blocs/client_create_order_state.dart';
|
||||
import '../navigation/client_create_order_navigator.dart';
|
||||
import '../widgets/order_type_card.dart';
|
||||
|
||||
/// One-time helper to map keys to translations since they are dynamic in BLoC state
|
||||
String _getTranslation(String key) {
|
||||
// Safe mapping - explicit keys expected
|
||||
/// 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') {
|
||||
@@ -31,7 +32,10 @@ String _getTranslation(String key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
/// Main entry page for the client create order flow.
|
||||
/// Allows the user to select the type of order they want to create.
|
||||
class ClientCreateOrderPage extends StatelessWidget {
|
||||
/// Creates a [ClientCreateOrderPage].
|
||||
const ClientCreateOrderPage({super.key});
|
||||
|
||||
@override
|
||||
@@ -50,22 +54,10 @@ class _CreateOrderView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: UiColors.white,
|
||||
elevation: 0,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1.0),
|
||||
child: Container(color: UiColors.border, height: 1.0),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary),
|
||||
onPressed: () => Modular.to.pop(),
|
||||
),
|
||||
title: Text(
|
||||
t.client_create_order.title,
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
backgroundColor: UiColors.bgPrimary,
|
||||
appBar: UiAppBar(
|
||||
title: t.client_create_order.title,
|
||||
onLeadingPressed: () => Modular.to.pop(),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
@@ -75,7 +67,7 @@ class _CreateOrderView extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space6),
|
||||
child: Text(
|
||||
@@ -102,19 +94,22 @@ class _CreateOrderView extends StatelessWidget {
|
||||
),
|
||||
itemCount: state.orderTypes.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final CreateOrderType type = state.orderTypes[index];
|
||||
return _OrderTypeCard(
|
||||
icon: type.icon,
|
||||
title: _getTranslation(type.titleKey),
|
||||
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(
|
||||
type.descriptionKey,
|
||||
key: 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':
|
||||
@@ -147,68 +142,92 @@ 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({
|
||||
/// Metadata for styling order type cards based on their ID.
|
||||
class _OrderTypeUiMetadata {
|
||||
const _OrderTypeUiMetadata({
|
||||
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,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: iconBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body2b.copyWith(color: textColor),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
description,
|
||||
style: UiTypography.footnote1r.copyWith(color: 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,32 +1,208 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/one_time_order_bloc.dart';
|
||||
import '../blocs/one_time_order_event.dart';
|
||||
import '../blocs/one_time_order_state.dart';
|
||||
import '../widgets/one_time_order/one_time_order_date_picker.dart';
|
||||
import '../widgets/one_time_order/one_time_order_location_input.dart';
|
||||
import '../widgets/one_time_order/one_time_order_position_card.dart';
|
||||
import '../widgets/one_time_order/one_time_order_section_header.dart';
|
||||
import '../widgets/one_time_order/one_time_order_success_view.dart';
|
||||
|
||||
/// Page for creating a one-time staffing order.
|
||||
/// Users can specify the date, location, and multiple staff positions required.
|
||||
class OneTimeOrderPage extends StatelessWidget {
|
||||
/// Creates a [OneTimeOrderPage].
|
||||
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(),
|
||||
),
|
||||
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('One-Time Order Flow (WIP)',
|
||||
style: UiTypography.body1r.textSecondary),
|
||||
),
|
||||
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 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,31 +1,39 @@
|
||||
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.
|
||||
/// Placeholder for future implementation.
|
||||
class PermanentOrderPage extends StatelessWidget {
|
||||
/// Creates a [PermanentOrderPage].
|
||||
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),
|
||||
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),
|
||||
),
|
||||
backgroundColor: UiColors.bgPrimary,
|
||||
appBar: UiAppBar(
|
||||
title: labels.title,
|
||||
onLeadingPressed: () => Modular.to.pop(),
|
||||
),
|
||||
body: Center(
|
||||
child: Text('Permanent Order Flow (WIP)',
|
||||
style: UiTypography.body1r.textSecondary),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
labels.subtitle,
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,326 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../blocs/rapid_order_bloc.dart';
|
||||
import '../blocs/rapid_order_event.dart';
|
||||
import '../blocs/rapid_order_state.dart';
|
||||
import '../widgets/rapid_order/rapid_order_example_card.dart';
|
||||
import '../widgets/rapid_order/rapid_order_header.dart';
|
||||
import '../widgets/rapid_order/rapid_order_success_view.dart';
|
||||
|
||||
/// Rapid Order Flow Page - Emergency staffing requests.
|
||||
/// Features voice recognition simulation and quick example selection.
|
||||
class RapidOrderPage extends StatelessWidget {
|
||||
/// Creates a [RapidOrderPage].
|
||||
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) {
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
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.
|
||||
/// Placeholder for future implementation.
|
||||
class RecurringOrderPage extends StatelessWidget {
|
||||
/// Creates a [RecurringOrderPage].
|
||||
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),
|
||||
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),
|
||||
),
|
||||
backgroundColor: UiColors.bgPrimary,
|
||||
appBar: UiAppBar(
|
||||
title: labels.title,
|
||||
onLeadingPressed: () => Modular.to.pop(),
|
||||
),
|
||||
body: Center(
|
||||
child: Text('Recurring Order Flow (WIP)',
|
||||
style: UiTypography.body1r.textSecondary),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
labels.subtitle,
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// A date picker field for the one-time order form.
|
||||
class OneTimeOrderDatePicker extends StatelessWidget {
|
||||
/// The label text to display above the field.
|
||||
final String label;
|
||||
|
||||
/// The currently selected date.
|
||||
final DateTime value;
|
||||
|
||||
/// Callback when a new date is selected.
|
||||
final ValueChanged<DateTime> onChanged;
|
||||
|
||||
/// Creates a [OneTimeOrderDatePicker].
|
||||
const OneTimeOrderDatePicker({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A location input field for the one-time order form.
|
||||
class OneTimeOrderLocationInput extends StatelessWidget {
|
||||
/// The label text to display above the field.
|
||||
final String label;
|
||||
|
||||
/// The current location value.
|
||||
final String value;
|
||||
|
||||
/// Callback when the location text changes.
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
/// Creates a [OneTimeOrderLocationInput].
|
||||
const OneTimeOrderLocationInput({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UiTextField(
|
||||
label: label,
|
||||
hintText: 'Select Branch/Location',
|
||||
controller: TextEditingController(text: value)
|
||||
..selection = TextSelection.collapsed(offset: value.length),
|
||||
onChanged: onChanged,
|
||||
prefixIcon: UiIcons.mapPin,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// A card widget for editing a specific position in a one-time order.
|
||||
class OneTimeOrderPositionCard extends StatelessWidget {
|
||||
/// The index of the position in the list.
|
||||
final int index;
|
||||
|
||||
/// The position entity data.
|
||||
final OneTimeOrderPosition position;
|
||||
|
||||
/// Whether this position can be removed (usually if there's more than one).
|
||||
final bool isRemovable;
|
||||
|
||||
/// Callback when the position data is updated.
|
||||
final ValueChanged<OneTimeOrderPosition> onUpdated;
|
||||
|
||||
/// Callback when the position is removed.
|
||||
final VoidCallback onRemoved;
|
||||
|
||||
/// Label for positions (e.g., "Position").
|
||||
final String positionLabel;
|
||||
|
||||
/// Label for the role selection.
|
||||
final String roleLabel;
|
||||
|
||||
/// Label for the worker count.
|
||||
final String workersLabel;
|
||||
|
||||
/// Label for the start time.
|
||||
final String startLabel;
|
||||
|
||||
/// Label for the end time.
|
||||
final String endLabel;
|
||||
|
||||
/// Label for the lunch break.
|
||||
final String lunchLabel;
|
||||
|
||||
/// Creates a [OneTimeOrderPositionCard].
|
||||
const OneTimeOrderPositionCard({
|
||||
required this.index,
|
||||
required this.position,
|
||||
required this.isRemovable,
|
||||
required this.onUpdated,
|
||||
required this.onRemoved,
|
||||
required this.positionLabel,
|
||||
required this.roleLabel,
|
||||
required this.workersLabel,
|
||||
required this.startLabel,
|
||||
required this.endLabel,
|
||||
required this.lunchLabel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'$positionLabel #${index + 1}',
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
if (isRemovable)
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.delete,
|
||||
size: 20, color: UiColors.destructive),
|
||||
onPressed: onRemoved,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: UiConstants.space6),
|
||||
|
||||
// Role (Dropdown)
|
||||
_LabelField(
|
||||
label: roleLabel,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: position.role.isEmpty ? null : position.role,
|
||||
items: <String>['Server', 'Bartender', 'Cook', 'Busser', 'Host']
|
||||
.map((String role) => DropdownMenuItem<String>(
|
||||
value: role,
|
||||
child:
|
||||
Text(role, style: UiTypography.body1r.textPrimary),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (String? val) {
|
||||
if (val != null) {
|
||||
onUpdated(position.copyWith(role: val));
|
||||
}
|
||||
},
|
||||
decoration: _inputDecoration(UiIcons.briefcase),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Count (Counter)
|
||||
_LabelField(
|
||||
label: workersLabel,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
_CounterButton(
|
||||
icon: UiIcons.minus,
|
||||
onPressed: position.count > 1
|
||||
? () => onUpdated(
|
||||
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: () =>
|
||||
onUpdated(position.copyWith(count: position.count + 1)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Start/End Time
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _LabelField(
|
||||
label: startLabel,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final TimeOfDay? picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 9, minute: 0),
|
||||
);
|
||||
if (picked != null) {
|
||||
onUpdated(position.copyWith(
|
||||
startTime: picked.format(context)));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: _boxDecoration(),
|
||||
child: Text(
|
||||
position.startTime.isEmpty
|
||||
? '--:--'
|
||||
: position.startTime,
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: _LabelField(
|
||||
label: endLabel,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final TimeOfDay? picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 17, minute: 0),
|
||||
);
|
||||
if (picked != null) {
|
||||
onUpdated(
|
||||
position.copyWith(endTime: picked.format(context)));
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: _boxDecoration(),
|
||||
child: Text(
|
||||
position.endTime.isEmpty ? '--:--' : position.endTime,
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Lunch Break
|
||||
_LabelField(
|
||||
label: lunchLabel,
|
||||
child: DropdownButtonFormField<int>(
|
||||
value: position.lunchBreak,
|
||||
items: <int>[0, 30, 45, 60]
|
||||
.map((int mins) => DropdownMenuItem<int>(
|
||||
value: mins,
|
||||
child: Text('${mins}m',
|
||||
style: UiTypography.body1r.textPrimary),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (int? val) {
|
||||
if (val != null) {
|
||||
onUpdated(position.copyWith(lunchBreak: val));
|
||||
}
|
||||
},
|
||||
decoration: _inputDecoration(UiIcons.clock),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A header widget for sections in the one-time order form.
|
||||
class OneTimeOrderSectionHeader extends StatelessWidget {
|
||||
/// The title text for the section.
|
||||
final String title;
|
||||
|
||||
/// Optional label for an action button on the right.
|
||||
final String? actionLabel;
|
||||
|
||||
/// Callback when the action button is tapped.
|
||||
final VoidCallback? onAction;
|
||||
|
||||
/// Creates a [OneTimeOrderSectionHeader].
|
||||
const OneTimeOrderSectionHeader({
|
||||
required this.title,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A view to display when a one-time order has been successfully created.
|
||||
class OneTimeOrderSuccessView extends StatelessWidget {
|
||||
/// The title of the success message.
|
||||
final String title;
|
||||
|
||||
/// The body of the success message.
|
||||
final String message;
|
||||
|
||||
/// Label for the completion button.
|
||||
final String buttonLabel;
|
||||
|
||||
/// Callback when the completion button is tapped.
|
||||
final VoidCallback onDone;
|
||||
|
||||
/// Creates a [OneTimeOrderSuccessView].
|
||||
const OneTimeOrderSuccessView({
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.buttonLabel,
|
||||
required this.onDone,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.white,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.tagSuccess,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(UiIcons.check,
|
||||
size: 50, color: UiColors.textSuccess),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.headline2m.textPrimary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
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,95 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A card widget representing an order type in the creation flow.
|
||||
class OrderTypeCard extends StatelessWidget {
|
||||
/// Icon to display at the top of the card.
|
||||
final IconData icon;
|
||||
|
||||
/// Main title of the order type.
|
||||
final String title;
|
||||
|
||||
/// Brief description of what this order type entails.
|
||||
final String description;
|
||||
|
||||
/// Background color of the card.
|
||||
final Color backgroundColor;
|
||||
|
||||
/// Color of the card's border.
|
||||
final Color borderColor;
|
||||
|
||||
/// Background color for the icon container.
|
||||
final Color iconBackgroundColor;
|
||||
|
||||
/// Color of the icon itself.
|
||||
final Color iconColor;
|
||||
|
||||
/// Color of the title text.
|
||||
final Color textColor;
|
||||
|
||||
/// Color of the description text.
|
||||
final Color descriptionColor;
|
||||
|
||||
/// Callback when the card is tapped.
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// Creates an [OrderTypeCard].
|
||||
const OrderTypeCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.backgroundColor,
|
||||
required this.borderColor,
|
||||
required this.iconBackgroundColor,
|
||||
required this.iconColor,
|
||||
required this.textColor,
|
||||
required this.descriptionColor,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: iconBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body2b.copyWith(color: textColor),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
style:
|
||||
UiTypography.footnote1r.copyWith(color: descriptionColor),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A card displaying an example message for a rapid order.
|
||||
class RapidOrderExampleCard extends StatelessWidget {
|
||||
/// The example text.
|
||||
final String example;
|
||||
|
||||
/// Whether this is the first (highlighted) example.
|
||||
final bool isHighlighted;
|
||||
|
||||
/// The label for the example prefix (e.g., "Example:").
|
||||
final String label;
|
||||
|
||||
/// Callback when the card is tapped.
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// Creates a [RapidOrderExampleCard].
|
||||
const RapidOrderExampleCard({
|
||||
required this.example,
|
||||
required this.isHighlighted,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isHighlighted
|
||||
? UiColors.accent.withValues(alpha: 0.15)
|
||||
: UiColors.white,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: Border.all(
|
||||
color: isHighlighted ? UiColors.accent : UiColors.border,
|
||||
),
|
||||
),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: label,
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
TextSpan(text: ' $example'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A header widget for the rapid order flow with a gradient background.
|
||||
class RapidOrderHeader extends StatelessWidget {
|
||||
/// The title of the page.
|
||||
final String title;
|
||||
|
||||
/// The subtitle or description.
|
||||
final String subtitle;
|
||||
|
||||
/// The formatted current date.
|
||||
final String date;
|
||||
|
||||
/// The formatted current time.
|
||||
final String time;
|
||||
|
||||
/// Callback when the back button is pressed.
|
||||
final VoidCallback onBack;
|
||||
|
||||
/// Creates a [RapidOrderHeader].
|
||||
const RapidOrderHeader({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.date,
|
||||
required this.time,
|
||||
required this.onBack,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
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: 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>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.zap,
|
||||
color: UiColors.accent,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.headline3m.copyWith(
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
date,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
time,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A view to display when a rapid order has been successfully created.
|
||||
class RapidOrderSuccessView extends StatelessWidget {
|
||||
/// The title of the success message.
|
||||
final String title;
|
||||
|
||||
/// The body of the success message.
|
||||
final String message;
|
||||
|
||||
/// Label for the completion button.
|
||||
final String buttonLabel;
|
||||
|
||||
/// Callback when the completion button is tapped.
|
||||
final VoidCallback onDone;
|
||||
|
||||
/// Creates a [RapidOrderSuccessView].
|
||||
const RapidOrderSuccessView({
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.buttonLabel,
|
||||
required this.onDone,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
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(
|
||||
title,
|
||||
style: UiTypography.headline1m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: UiButton.primary(
|
||||
text: buttonLabel,
|
||||
onPressed: onDone,
|
||||
size: UiButtonSize.large,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user