feat: Implement permanent and recurring order features with BLoC architecture

- Added PermanentOrderEvent and PermanentOrderState to manage permanent order events and states.
- Created RapidOrderBloc, RapidOrderEvent, and RapidOrderState for handling rapid order creation.
- Introduced RecurringOrderBloc, RecurringOrderEvent, and RecurringOrderState for managing recurring orders.
- Developed utility classes for order types and UI metadata for styling order type cards.
- Enhanced validation logic for order states to ensure data integrity.
- Integrated vendor and hub loading functionalities for both permanent and recurring orders.
This commit is contained in:
Achintha Isuru
2026-02-21 18:11:47 -05:00
parent 9e0ca1ef96
commit f3eb33a303
41 changed files with 138 additions and 246 deletions

View File

@@ -221,6 +221,14 @@ class UiTypography {
color: UiColors.textPrimary,
);
/// Headline 4 Bold - Font: Instrument Sans, Size: 20, Height: 1.5 (#121826)
static final TextStyle headline4b = _primaryBase.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
height: 1.5,
color: UiColors.textPrimary,
);
/// Headline 5 Regular - Font: Instrument Sans, Size: 18, Height: 1.5 (#121826)
static final TextStyle headline5r = _primaryBase.copyWith(
fontWeight: FontWeight.w400,

View File

@@ -1,3 +1,4 @@
import 'package:design_system/src/ui_typography.dart';
import 'package:flutter/material.dart';
import '../ui_icons.dart';
@@ -14,7 +15,7 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
this.leading,
this.actions,
this.height = kToolbarHeight,
this.centerTitle = true,
this.centerTitle = false,
this.onLeadingPressed,
this.showBackButton = true,
this.bottom,
@@ -56,6 +57,7 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
(title != null
? Text(
title!,
style: UiTypography.headline4b,
)
: null),
leading: leading ??

View File

@@ -8,12 +8,7 @@ import 'domain/usecases/create_one_time_order_usecase.dart';
import 'domain/usecases/create_permanent_order_usecase.dart';
import 'domain/usecases/create_recurring_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/permanent_order_bloc.dart';
import 'presentation/blocs/recurring_order_bloc.dart';
import 'presentation/blocs/rapid_order_bloc.dart';
import 'presentation/blocs/index.dart';
import 'presentation/pages/create_order_page.dart';
import 'presentation/pages/one_time_order_page.dart';
import 'presentation/pages/permanent_order_page.dart';
@@ -35,14 +30,12 @@ class ClientCreateOrderModule extends Module {
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(ClientCreateOrderRepositoryImpl.new);
// UseCases
i.addLazySingleton(GetOrderTypesUseCase.new);
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
i.addLazySingleton(CreatePermanentOrderUseCase.new);
i.addLazySingleton(CreateRecurringOrderUseCase.new);
i.addLazySingleton(CreateRapidOrderUseCase.new);
// BLoCs
i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
i.add<RapidOrderBloc>(RapidOrderBloc.new);
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
i.add<PermanentOrderBloc>(PermanentOrderBloc.new);

View File

@@ -18,39 +18,6 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
final dc.DataConnectService _service;
@override
Future<List<domain.OrderType>> getOrderTypes() {
return Future<List<domain.OrderType>>.value(const <domain.OrderType>[
domain.OrderType(
id: 'one-time',
titleKey: 'client_create_order.types.one_time',
descriptionKey: 'client_create_order.types.one_time_desc',
),
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
// domain.OrderType(
// id: 'rapid',
// titleKey: 'client_create_order.types.rapid',
// descriptionKey: 'client_create_order.types.rapid_desc',
// ),
domain.OrderType(
id: 'recurring',
titleKey: 'client_create_order.types.recurring',
descriptionKey: 'client_create_order.types.recurring_desc',
),
// domain.OrderType(
// id: 'permanent',
// titleKey: 'client_create_order.types.permanent',
// descriptionKey: 'client_create_order.types.permanent_desc',
// ),
domain.OrderType(
id: 'permanent',
titleKey: 'client_create_order.types.permanent',
descriptionKey: 'client_create_order.types.permanent_desc',
),
]);
}
@override
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
return _service.run(() async {

View File

@@ -3,15 +3,11 @@ import 'package:krow_domain/krow_domain.dart';
/// Interface for the Client Create Order repository.
///
/// This repository is responsible for:
/// 1. Retrieving available order types for the client.
/// 2. Submitting different types of staffing orders (Rapid, One-Time).
/// 1. Submitting different types of staffing orders (Rapid, One-Time, Recurring, Permanent).
///
/// It follows the KROW Clean Architecture by defining the contract in the
/// domain layer, to be implemented in the data layer.
abstract interface class ClientCreateOrderRepositoryInterface {
/// Retrieves the list of available order types (e.g., Rapid, One-Time, Recurring).
Future<List<OrderType>> getOrderTypes();
/// Submits a one-time staffing order with specific details.
///
/// [order] contains the date, location, and required positions.

View File

@@ -1,20 +0,0 @@
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 fetches the list of supported staffing order types
/// from the [ClientCreateOrderRepositoryInterface].
class GetOrderTypesUseCase implements NoInputUseCase<List<OrderType>> {
/// Creates a [GetOrderTypesUseCase].
///
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
const GetOrderTypesUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository;
@override
Future<List<OrderType>> call() {
return _repository.getOrderTypes();
}
}

View File

@@ -1,32 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.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>
with BlocErrorHandler<ClientCreateOrderState> {
ClientCreateOrderBloc(this._getOrderTypesUseCase)
: super(const ClientCreateOrderInitial()) {
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
}
final GetOrderTypesUseCase _getOrderTypesUseCase;
Future<void> _onTypesRequested(
ClientCreateOrderTypesRequested event,
Emitter<ClientCreateOrderState> emit,
) async {
await handleError(
emit: emit.call,
action: () async {
final List<OrderType> types = await _getOrderTypesUseCase();
emit(ClientCreateOrderLoadSuccess(types));
},
onError: (String errorKey) => ClientCreateOrderLoadFailure(errorKey),
);
}
}

View File

@@ -1,20 +0,0 @@
import 'package:equatable/equatable.dart';
abstract class ClientCreateOrderEvent extends Equatable {
const ClientCreateOrderEvent();
@override
List<Object?> get props => <Object?>[];
}
class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent {
const ClientCreateOrderTypesRequested();
}
class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent {
const ClientCreateOrderTypeSelected(this.typeId);
final String typeId;
@override
List<Object?> get props => <Object?>[typeId];
}

View File

@@ -1,36 +0,0 @@
import 'package:equatable/equatable.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 => <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 {
const ClientCreateOrderLoadSuccess(this.orderTypes);
/// The list of available order types retrieved from the domain.
final List<OrderType> orderTypes;
@override
List<Object?> get props => <Object?>[orderTypes];
}
/// State representing a failure to load order types.
class ClientCreateOrderLoadFailure extends ClientCreateOrderState {
const ClientCreateOrderLoadFailure(this.error);
final String error;
@override
List<Object?> get props => <Object?>[error];
}

View File

@@ -0,0 +1,4 @@
export 'one_time_order/index.dart';
export 'rapid_order/index.dart';
export 'recurring_order/index.dart';
export 'permanent_order/index.dart';

View File

@@ -0,0 +1,3 @@
export 'one_time_order_bloc.dart';
export 'one_time_order_event.dart';
export 'one_time_order_state.dart';

View File

@@ -1,10 +1,11 @@
import 'package:client_create_order/src/domain/arguments/one_time_order_arguments.dart';
import 'package:client_create_order/src/domain/usecases/create_one_time_order_usecase.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
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';

View File

@@ -0,0 +1,3 @@
export 'permanent_order_bloc.dart';
export 'permanent_order_event.dart';
export 'permanent_order_state.dart';

View File

@@ -1,9 +1,10 @@
import 'package:client_create_order/src/domain/usecases/create_permanent_order_usecase.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/usecases/create_permanent_order_usecase.dart';
import 'permanent_order_event.dart';
import 'permanent_order_state.dart';

View File

@@ -0,0 +1,3 @@
export 'rapid_order_bloc.dart';
export 'rapid_order_event.dart';
export 'rapid_order_state.dart';

View File

@@ -1,7 +1,8 @@
import 'package:client_create_order/src/domain/arguments/rapid_order_arguments.dart';
import 'package:client_create_order/src/domain/usecases/create_rapid_order_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.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';

View File

@@ -0,0 +1,3 @@
export 'recurring_order_bloc.dart';
export 'recurring_order_event.dart';
export 'recurring_order_state.dart';

View File

@@ -1,9 +1,10 @@
import 'package:client_create_order/src/domain/usecases/create_recurring_order_usecase.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/usecases/create_recurring_order_usecase.dart';
import 'recurring_order_event.dart';
import 'recurring_order_state.dart';

View File

@@ -1,26 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/client_create_order_bloc.dart';
import '../blocs/client_create_order_event.dart';
import '../widgets/create_order/create_order_view.dart';
/// Main entry page for the client create order flow.
///
/// This page initializes the [ClientCreateOrderBloc] and displays the [CreateOrderView].
/// This page displays the [CreateOrderView].
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
/// delegating its state and UI to other components.
/// delegating its UI to other components.
class ClientCreateOrderPage extends StatelessWidget {
/// Creates a [ClientCreateOrderPage].
const ClientCreateOrderPage({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<ClientCreateOrderBloc>(
create: (BuildContext context) =>
Modular.get<ClientCreateOrderBloc>()
..add(const ClientCreateOrderTypesRequested()),
child: const CreateOrderView(),
);
return const CreateOrderView();
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/one_time_order_bloc.dart';
import '../blocs/one_time_order/one_time_order_bloc.dart';
import '../widgets/one_time_order/one_time_order_view.dart';
/// Page for creating a one-time staffing order.

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/permanent_order_bloc.dart';
import '../blocs/permanent_order/permanent_order_bloc.dart';
import '../widgets/permanent_order/permanent_order_view.dart';
/// Page for creating a permanent staffing order.

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/rapid_order_bloc.dart';
import '../blocs/rapid_order/rapid_order_bloc.dart';
import '../widgets/rapid_order/rapid_order_view.dart';
/// Rapid Order Flow Page - Emergency staffing requests.

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/recurring_order_bloc.dart';
import '../blocs/recurring_order/recurring_order_bloc.dart';
import '../widgets/recurring_order/recurring_order_view.dart';
/// Page for creating a recurring staffing order.

View File

@@ -0,0 +1,32 @@
import 'package:krow_domain/krow_domain.dart' as domain;
/// Order type constants for the create order feature
final List<domain.OrderType> orderTypes = const <domain.OrderType>[
domain.OrderType(
id: 'one-time',
titleKey: 'client_create_order.types.one_time',
descriptionKey: 'client_create_order.types.one_time_desc',
),
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
// domain.OrderType(
// id: 'rapid',
// titleKey: 'client_create_order.types.rapid',
// descriptionKey: 'client_create_order.types.rapid_desc',
// ),
domain.OrderType(
id: 'recurring',
titleKey: 'client_create_order.types.recurring',
descriptionKey: 'client_create_order.types.recurring_desc',
),
// domain.OrderType(
// id: 'permanent',
// titleKey: 'client_create_order.types.permanent',
// descriptionKey: 'client_create_order.types.permanent_desc',
// ),
domain.OrderType(
id: 'permanent',
titleKey: 'client_create_order.types.permanent',
descriptionKey: 'client_create_order.types.permanent_desc',
),
];

View File

@@ -1,13 +1,11 @@
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_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../blocs/client_create_order_bloc.dart';
import '../../blocs/client_create_order_state.dart';
import '../../ui_entities/order_type_ui_metadata.dart';
import '../../utils/order_types.dart';
import '../../utils/ui_entities/order_type_ui_metadata.dart';
import '../order_type_card.dart';
/// Helper to map keys to localized strings.
@@ -64,58 +62,50 @@ class CreateOrderView extends StatelessWidget {
),
),
Expanded(
child: BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
builder:
(BuildContext context, ClientCreateOrderState state) {
if (state is ClientCreateOrderLoadSuccess) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: UiConstants.space4,
crossAxisSpacing: UiConstants.space4,
childAspectRatio: 1,
),
itemCount: state.orderTypes.length,
itemBuilder: (BuildContext context, int index) {
final OrderType type = state.orderTypes[index];
final OrderTypeUiMetadata ui =
OrderTypeUiMetadata.fromId(id: type.id);
child: GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: UiConstants.space4,
crossAxisSpacing: UiConstants.space4,
childAspectRatio: 1,
),
itemCount: orderTypes.length,
itemBuilder: (BuildContext context, int index) {
final OrderType type = orderTypes[index];
final OrderTypeUiMetadata ui =
OrderTypeUiMetadata.fromId(id: type.id);
return OrderTypeCard(
icon: ui.icon,
title: _getTranslation(key: type.titleKey),
description: _getTranslation(
key: type.descriptionKey,
),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.descriptionColor,
onTap: () {
switch (type.id) {
case 'rapid':
Modular.to.toCreateOrderRapid();
break;
case 'one-time':
Modular.to.toCreateOrderOneTime();
break;
case 'recurring':
Modular.to.toCreateOrderRecurring();
break;
case 'permanent':
Modular.to.toCreateOrderPermanent();
break;
}
},
);
},
);
return OrderTypeCard(
icon: ui.icon,
title: _getTranslation(key: type.titleKey),
description: _getTranslation(
key: type.descriptionKey,
),
backgroundColor: ui.backgroundColor,
borderColor: ui.borderColor,
iconBackgroundColor: ui.iconBackgroundColor,
iconColor: ui.iconColor,
textColor: ui.textColor,
descriptionColor: ui.descriptionColor,
onTap: () {
switch (type.id) {
case 'rapid':
Modular.to.toCreateOrderRapid();
break;
case 'one-time':
Modular.to.toCreateOrderOneTime();
break;
case 'recurring':
Modular.to.toCreateOrderRecurring();
break;
case 'permanent':
Modular.to.toCreateOrderPermanent();
break;
}
return const Center(child: CircularProgressIndicator());
},
);
},
),
),
],
@@ -124,4 +114,4 @@ class CreateOrderView extends StatelessWidget {
),
);
}
}
}

View File

@@ -2,7 +2,7 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../blocs/one_time_order_state.dart';
import '../../blocs/one_time_order/one_time_order_state.dart';
/// A card widget for editing a specific position in a one-time order.
/// Matches the prototype layout while using design system tokens.

View File

@@ -5,9 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.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 '../../blocs/one_time_order/one_time_order_bloc.dart';
import '../../blocs/one_time_order/one_time_order_event.dart';
import '../../blocs/one_time_order/one_time_order_state.dart';
import 'one_time_order_date_picker.dart';
import 'one_time_order_event_name_input.dart';
import 'one_time_order_header.dart';

View File

@@ -1,7 +1,7 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../blocs/permanent_order_state.dart';
import '../../blocs/permanent_order/permanent_order_state.dart';
/// A card widget for editing a specific position in a permanent order.
class PermanentOrderPositionCard extends StatelessWidget {

View File

@@ -5,9 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../../blocs/permanent_order_bloc.dart';
import '../../blocs/permanent_order_event.dart';
import '../../blocs/permanent_order_state.dart';
import '../../blocs/permanent_order/permanent_order_bloc.dart';
import '../../blocs/permanent_order/permanent_order_event.dart';
import '../../blocs/permanent_order/permanent_order_state.dart';
import 'permanent_order_date_picker.dart';
import 'permanent_order_event_name_input.dart';
import 'permanent_order_header.dart';

View File

@@ -5,9 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import '../../blocs/rapid_order_bloc.dart';
import '../../blocs/rapid_order_event.dart';
import '../../blocs/rapid_order_state.dart';
import '../../blocs/rapid_order/rapid_order_bloc.dart';
import '../../blocs/rapid_order/rapid_order_event.dart';
import '../../blocs/rapid_order/rapid_order_state.dart';
import 'rapid_order_example_card.dart';
import 'rapid_order_header.dart';
import 'rapid_order_success_view.dart';

View File

@@ -1,7 +1,7 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../blocs/recurring_order_state.dart';
import '../../blocs/recurring_order/recurring_order_state.dart';
/// A card widget for editing a specific position in a recurring order.
class RecurringOrderPositionCard extends StatelessWidget {

View File

@@ -5,9 +5,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import '../../blocs/recurring_order_bloc.dart';
import '../../blocs/recurring_order_event.dart';
import '../../blocs/recurring_order_state.dart';
import '../../blocs/recurring_order/recurring_order_bloc.dart';
import '../../blocs/recurring_order/recurring_order_event.dart';
import '../../blocs/recurring_order/recurring_order_state.dart';
import 'recurring_order_date_picker.dart';
import 'recurring_order_event_name_input.dart';
import 'recurring_order_header.dart';