Add rapid order parsing & audio recording

Add support for transcribing and parsing rapid (urgent) orders into one-time order drafts. Introduces ParseRapidOrderTextToOrderUseCase and wiring for TranscribeRapidOrderUseCase, implements parseRapidOrder and transcribeRapidOrder in the client repository, and injects these into the RapidOrderBloc and module. Adds the record package dependency and registers the record plugin for iOS/macOS targets. Updates OneTimeOrder state, bloc and views to handle rapid-order drafts and navigate to the one-time order flow after parsing. Also includes small formatting and navigator changes.
This commit is contained in:
Achintha Isuru
2026-02-27 13:16:11 -05:00
parent 5584c21a49
commit 9d25fd44cc
19 changed files with 256 additions and 32 deletions

View File

@@ -36,6 +36,12 @@
@import image_picker_ios;
#endif
#if __has_include(<record_ios/RecordIosPlugin.h>)
#import <record_ios/RecordIosPlugin.h>
#else
@import record_ios;
#endif
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
#else
@@ -56,6 +62,7 @@
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
[RecordIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordIosPlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
}

View File

@@ -10,6 +10,7 @@ import file_selector_macos
import firebase_app_check
import firebase_auth
import firebase_core
import record_macos
import shared_preferences_foundation
import url_launcher_macos
@@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -54,6 +54,12 @@
@import permission_handler_apple;
#endif
#if __has_include(<record_ios/RecordIosPlugin.h>)
#import <record_ios/RecordIosPlugin.h>
#else
@import record_ios;
#endif
#if __has_include(<shared_preferences_foundation/SharedPreferencesPlugin.h>)
#import <shared_preferences_foundation/SharedPreferencesPlugin.h>
#else
@@ -77,6 +83,7 @@
[FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]];
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
[RecordIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordIosPlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
}

View File

@@ -11,6 +11,7 @@ import firebase_app_check
import firebase_auth
import firebase_core
import geolocator_apple
import record_macos
import shared_preferences_foundation
import url_launcher_macos
@@ -21,6 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -139,7 +139,7 @@ extension StaffNavigator on IModularNavigator {
///
/// Manage personal information, documents, and preferences.
void toProfile() {
pushNamedAndRemoveUntil(StaffPaths.profile, (_) => false);
navigate(StaffPaths.profile);
}
// ==========================================================================
@@ -189,7 +189,7 @@ extension StaffNavigator on IModularNavigator {
///
/// Record previous work experience and qualifications.
void toExperience() {
pushNamed(StaffPaths.experience);
navigate(StaffPaths.experience);
}
/// Pushes the attire preferences page.

View File

@@ -25,4 +25,5 @@ dependencies:
image_picker: ^1.1.2
path_provider: ^2.1.3
file_picker: ^8.1.7
record: ^6.2.0
firebase_auth: ^6.1.4

View File

@@ -9,6 +9,8 @@ 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_details_for_reorder_usecase.dart';
import 'domain/usecases/parse_rapid_order_usecase.dart';
import 'domain/usecases/transcribe_rapid_order_usecase.dart';
import 'presentation/blocs/index.dart';
import 'presentation/pages/create_order_page.dart';
import 'presentation/pages/one_time_order_page.dart';
@@ -37,10 +39,18 @@ class ClientCreateOrderModule extends Module {
i.addLazySingleton(CreatePermanentOrderUseCase.new);
i.addLazySingleton(CreateRecurringOrderUseCase.new);
i.addLazySingleton(CreateRapidOrderUseCase.new);
i.addLazySingleton(TranscribeRapidOrderUseCase.new);
i.addLazySingleton(ParseRapidOrderTextToOrderUseCase.new);
i.addLazySingleton(GetOrderDetailsForReorderUseCase.new);
// BLoCs
i.add<RapidOrderBloc>(RapidOrderBloc.new);
i.add<RapidOrderBloc>(
(Injector i) => RapidOrderBloc(
i.get<TranscribeRapidOrderUseCase>(),
i.get<ParseRapidOrderTextToOrderUseCase>(),
i.get<AudioRecorderService>(),
),
);
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
i.add<PermanentOrderBloc>(PermanentOrderBloc.new);
i.add<RecurringOrderBloc>(RecurringOrderBloc.new);

View File

@@ -367,6 +367,51 @@ class ClientCreateOrderRepositoryImpl
throw UnimplementedError('Rapid order IA is not connected yet.');
}
@override
Future<domain.OneTimeOrder> parseRapidOrder(String text) async {
final RapidOrderParseResponse response = await _rapidOrderService.parseText(
text: text,
);
final RapidOrderParsedData data = response.parsed;
final DateTime startAt =
DateTime.tryParse(data.startAt ?? '') ?? DateTime.now();
final DateTime endAt =
DateTime.tryParse(data.endAt ?? '') ??
startAt.add(const Duration(hours: 8));
final String startTimeStr = DateFormat('hh:mm a').format(startAt);
final String endTimeStr = DateFormat('hh:mm a').format(endAt);
return domain.OneTimeOrder(
date: startAt,
location: data.locationHint ?? '',
eventName: data.notes ?? '',
hub: data.locationHint != null
? domain.OneTimeOrderHubDetails(
id: '',
name: data.locationHint!,
address: '',
)
: null,
positions: data.positions.map((RapidOrderPosition p) {
return domain.OneTimeOrderPosition(
role: p.role,
count: p.count,
startTime: startTimeStr,
endTime: endTimeStr,
);
}).toList(),
);
}
@override
Future<String> transcribeRapidOrder(String audioPath) async {
final RapidOrderTranscriptionResponse response = await _rapidOrderService
.transcribeAudio(audioFileUri: audioPath);
return response.transcript;
}
@override
Future<void> reorder(String previousOrderId, DateTime newDate) async {
// TODO: Implement reorder functionality to fetch the previous order and create a new one with the updated date.

View File

@@ -24,6 +24,16 @@ abstract interface class ClientCreateOrderRepositoryInterface {
/// [description] is the text message (or transcribed voice) describing the need.
Future<void> createRapidOrder(String description);
/// Transcribes the audio file for a rapid order.
///
/// [audioPath] is the local path to the recorded audio file.
Future<String> transcribeRapidOrder(String audioPath);
/// Parses the text description for a rapid order into a structured draft.
///
/// [text] is the text message describing the need.
Future<OneTimeOrder> parseRapidOrder(String text);
/// Reorders an existing staffing order with a new date.
///
/// [previousOrderId] is the ID of the order to reorder.

View File

@@ -0,0 +1,15 @@
import 'package:krow_domain/krow_domain.dart';
import '../repositories/client_create_order_repository_interface.dart';
/// Use case for parsing rapid order text into a structured OneTimeOrder.
class ParseRapidOrderTextToOrderUseCase {
ParseRapidOrderTextToOrderUseCase({
required ClientCreateOrderRepositoryInterface repository,
}) : _repository = repository;
final ClientCreateOrderRepositoryInterface _repository;
Future<OneTimeOrder> call(String text) async {
return _repository.parseRapidOrder(text);
}
}

View File

@@ -136,9 +136,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
}
}
Future<void> _loadManagersForHub(
String hubId,
) async {
Future<void> _loadManagersForHub(String hubId) async {
final List<OneTimeOrderManagerOption>? managers =
await handleErrorWithResult(
action: () async {
@@ -163,7 +161,9 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
.toList();
},
onError: (_) {
add(const OneTimeOrderManagersLoaded(<OneTimeOrderManagerOption>[]));
add(
const OneTimeOrderManagersLoaded(<OneTimeOrderManagerOption>[]),
);
},
);
@@ -172,7 +172,6 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
}
}
Future<void> _onVendorsLoaded(
OneTimeOrderVendorsLoaded event,
Emitter<OneTimeOrderState> emit,
@@ -216,7 +215,6 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
}
}
void _onHubChanged(
OneTimeOrderHubChanged event,
Emitter<OneTimeOrderState> emit,
@@ -239,7 +237,6 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
emit(state.copyWith(managers: event.managers));
}
void _onEventNameChanged(
OneTimeOrderEventNameChanged event,
Emitter<OneTimeOrderState> emit,
@@ -349,6 +346,45 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
final DateTime? startDate = data['startDate'] as DateTime?;
final String? orderId = data['orderId']?.toString();
// Handle Rapid Order Draft
if (data['isRapidDraft'] == true) {
final OneTimeOrder? order = data['order'] as OneTimeOrder?;
if (order != null) {
emit(
state.copyWith(
eventName: order.eventName ?? '',
date: order.date,
positions: order.positions,
location: order.location,
isRapidDraft: true,
),
);
// Try to match vendor if available
if (order.vendorId != null) {
final Vendor? vendor = state.vendors
.where((Vendor v) => v.id == order.vendorId)
.firstOrNull;
if (vendor != null) {
emit(state.copyWith(selectedVendor: vendor));
await _loadRolesForVendor(vendor.id, emit);
}
}
// Try to match hub if available
if (order.hub != null) {
final OneTimeOrderHubOption? hub = state.hubs
.where((OneTimeOrderHubOption h) => h.id == order.hub?.id)
.firstOrNull;
if (hub != null) {
emit(state.copyWith(selectedHub: hub));
await _loadManagersForHub(hub.id);
}
}
return;
}
}
emit(state.copyWith(eventName: title, date: startDate ?? DateTime.now()));
if (orderId == null || orderId.isEmpty) return;

View File

@@ -18,6 +18,7 @@ class OneTimeOrderState extends Equatable {
this.roles = const <OneTimeOrderRoleOption>[],
this.managers = const <OneTimeOrderManagerOption>[],
this.selectedManager,
this.isRapidDraft = false,
});
factory OneTimeOrderState.initial() {
@@ -47,6 +48,7 @@ class OneTimeOrderState extends Equatable {
final List<OneTimeOrderRoleOption> roles;
final List<OneTimeOrderManagerOption> managers;
final OneTimeOrderManagerOption? selectedManager;
final bool isRapidDraft;
OneTimeOrderState copyWith({
DateTime? date,
@@ -62,6 +64,7 @@ class OneTimeOrderState extends Equatable {
List<OneTimeOrderRoleOption>? roles,
List<OneTimeOrderManagerOption>? managers,
OneTimeOrderManagerOption? selectedManager,
bool? isRapidDraft,
}) {
return OneTimeOrderState(
date: date ?? this.date,
@@ -77,6 +80,7 @@ class OneTimeOrderState extends Equatable {
roles: roles ?? this.roles,
managers: managers ?? this.managers,
selectedManager: selectedManager ?? this.selectedManager,
isRapidDraft: isRapidDraft ?? this.isRapidDraft,
);
}
@@ -109,6 +113,7 @@ class OneTimeOrderState extends Equatable {
roles,
managers,
selectedManager,
isRapidDraft,
];
}
@@ -171,10 +176,7 @@ class OneTimeOrderRoleOption extends Equatable {
}
class OneTimeOrderManagerOption extends Equatable {
const OneTimeOrderManagerOption({
required this.id,
required this.name,
});
const OneTimeOrderManagerOption({required this.id, required this.name});
final String id;
final String name;
@@ -182,4 +184,3 @@ class OneTimeOrderManagerOption extends Equatable {
@override
List<Object?> get props => <Object?>[id, name];
}

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:client_create_order/src/domain/usecases/parse_rapid_order_usecase.dart';
import 'package:client_create_order/src/domain/usecases/transcribe_rapid_order_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'rapid_order_event.dart';
import 'rapid_order_state.dart';
@@ -9,8 +10,11 @@ import 'rapid_order_state.dart';
/// BLoC for managing the rapid (urgent) order creation flow.
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
with BlocErrorHandler<RapidOrderState> {
RapidOrderBloc(this._createRapidOrderUseCase)
: super(
RapidOrderBloc(
this._transcribeRapidOrderUseCase,
this._parseRapidOrderUseCase,
this._audioRecorderService,
) : super(
const RapidOrderInitial(
examples: <String>[
'"We had a call out. Need 2 cooks ASAP"',
@@ -24,7 +28,9 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
on<RapidOrderSubmitted>(_onSubmitted);
on<RapidOrderExampleSelected>(_onExampleSelected);
}
final CreateRapidOrderUseCase _createRapidOrderUseCase;
final TranscribeRapidOrderUseCase _transcribeRapidOrderUseCase;
final ParseRapidOrderTextToOrderUseCase _parseRapidOrderUseCase;
final AudioRecorderService _audioRecorderService;
void _onMessageChanged(
RapidOrderMessageChanged event,
@@ -72,10 +78,8 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
await handleError(
emit: emit.call,
action: () async {
await _createRapidOrderUseCase(
RapidOrderArguments(description: message),
);
emit(const RapidOrderSuccess());
final OneTimeOrder order = await _parseRapidOrderUseCase(message);
emit(RapidOrderParsed(order));
},
onError: (String errorKey) => RapidOrderFailure(errorKey),
);

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class RapidOrderState extends Equatable {
const RapidOrderState();
@@ -48,3 +49,11 @@ class RapidOrderFailure extends RapidOrderState {
@override
List<Object?> get props => <Object?>[error];
}
class RapidOrderParsed extends RapidOrderState {
const RapidOrderParsed(this.order);
final OneTimeOrder order;
@override
List<Object?> get props => <Object?>[order];
}

View File

@@ -53,6 +53,7 @@ class OneTimeOrderPage extends StatelessWidget {
: null,
hubManagers: state.managers.map(_mapManager).toList(),
isValid: state.isValid,
title: state.isRapidDraft ? 'Rapid Order : Verify the order' : null,
onEventNameChanged: (String val) =>
bloc.add(OneTimeOrderEventNameChanged(val)),
onVendorChanged: (Vendor val) =>

View File

@@ -72,6 +72,13 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
TextPosition(offset: _messageController.text.length),
);
}
} else if (state is RapidOrderParsed) {
Modular.to.toCreateOrderOneTime(
arguments: <String, dynamic>{
'order': state.order,
'isRapidDraft': true,
},
);
} else if (state is RapidOrderFailure) {
UiSnackbar.show(
context,

View File

@@ -38,6 +38,7 @@ class OneTimeOrderView extends StatelessWidget {
required this.onSubmit,
required this.onDone,
required this.onBack,
this.title,
super.key,
});
@@ -54,6 +55,7 @@ class OneTimeOrderView extends StatelessWidget {
final List<OrderManagerUiModel> hubManagers;
final OrderManagerUiModel? selectedHubManager;
final bool isValid;
final String? title;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
@@ -61,7 +63,8 @@ class OneTimeOrderView extends StatelessWidget {
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
final void Function(int index) onPositionRemoved;
final VoidCallback onSubmit;
final VoidCallback onDone;
@@ -98,7 +101,7 @@ class OneTimeOrderView extends StatelessWidget {
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: labels.title,
title: title ?? labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
@@ -136,7 +139,7 @@ class OneTimeOrderView extends StatelessWidget {
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: labels.title,
title: title ?? labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
@@ -220,7 +223,8 @@ class _OneTimeOrderForm extends StatelessWidget {
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
final void Function(int index) onPositionRemoved;
@override
@@ -317,10 +321,7 @@ class _OneTimeOrderForm extends StatelessWidget {
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
child: Text(hub.name, style: UiTypography.body2m.textPrimary),
);
}).toList(),
),

View File

@@ -10,6 +10,7 @@ import file_selector_macos
import firebase_app_check
import firebase_auth
import firebase_core
import record_macos
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -18,5 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@@ -1221,6 +1221,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
record:
dependency: transitive
description:
name: record
sha256: d5b6b334f3ab02460db6544e08583c942dbf23e3504bf1e14fd4cbe3d9409277
url: "https://pub.dev"
source: hosted
version: "6.2.0"
record_android:
dependency: transitive
description:
name: record_android
sha256: "94783f08403aed33ffb68797bf0715b0812eb852f3c7985644c945faea462ba1"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
record_ios:
dependency: transitive
description:
name: record_ios
sha256: "8df7c136131bd05efc19256af29b2ba6ccc000ccc2c80d4b6b6d7a8d21a3b5a9"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: c31a35cc158cd666fc6395f7f56fc054f31685571684be6b97670a27649ce5c7
url: "https://pub.dev"
source: hosted
version: "1.3.0"
record_macos:
dependency: transitive
description:
name: record_macos
sha256: "084902e63fc9c0c224c29203d6c75f0bdf9b6a40536c9d916393c8f4c4256488"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: "8a81dbc4e14e1272a285bbfef6c9136d070a47d9b0d1f40aa6193516253ee2f6"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: "7e9846981c1f2d111d86f0ae3309071f5bba8b624d1c977316706f08fc31d16d"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78"
url: "https://pub.dev"
source: hosted
version: "1.0.7"
rename:
dependency: transitive
description: