Merge branch 'dev' into 493-implement-rapid-order-creation-voice-text-in-client-mobile-app

This commit is contained in:
Achintha Isuru
2026-02-27 15:32:11 -05:00
20 changed files with 228 additions and 98 deletions

View File

@@ -66,7 +66,8 @@ help:
@echo " make dataconnect-deploy [ENV=dev] Deploy Data Connect schemas (dev/staging)" @echo " make dataconnect-deploy [ENV=dev] Deploy Data Connect schemas (dev/staging)"
@echo " make dataconnect-sql-migrate [ENV=dev] Apply pending SQL migrations" @echo " make dataconnect-sql-migrate [ENV=dev] Apply pending SQL migrations"
@echo " make dataconnect-generate-sdk [ENV=dev] Regenerate Data Connect client SDK" @echo " make dataconnect-generate-sdk [ENV=dev] Regenerate Data Connect client SDK"
@echo " make dataconnect-sync [ENV=dev] Full sync: deploy + migrate + generate SDK" @echo " make dataconnect-sync [ENV=dev] Fast sync: deploy connector + generate SDK"
@echo " make dataconnect-sync-full [ENV=dev] Full sync: deploy + migrate + generate SDK"
@echo " make dataconnect-seed [ENV=dev] Seed database with test data" @echo " make dataconnect-seed [ENV=dev] Seed database with test data"
@echo " make dataconnect-clean [ENV=dev] Delete all data from Data Connect" @echo " make dataconnect-clean [ENV=dev] Delete all data from Data Connect"
@echo " make dataconnect-test [ENV=dev] Test Data Connect deployment (dry-run)" @echo " make dataconnect-test [ENV=dev] Test Data Connect deployment (dry-run)"
@@ -101,6 +102,6 @@ help:
@echo "" @echo ""
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@echo " 💡 Tip: Run 'make mobile-install' first for mobile development" @echo " 💡 Tip: Run 'make mobile-install' first for mobile development"
@echo " 💡 Tip: Use 'make dataconnect-sync' after schema changes" @echo " 💡 Tip: Use 'make dataconnect-sync-full' after schema changes"
@echo " 💡 Tip: Default ENV=dev, use ENV=staging for staging environment" @echo " 💡 Tip: Default ENV=dev, use ENV=staging for staging environment"
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

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

View File

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

View File

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

View File

@@ -27,4 +27,3 @@ dependencies:
file_picker: ^8.1.7 file_picker: ^8.1.7
record: ^6.2.0 record: ^6.2.0
firebase_auth: ^6.1.4 firebase_auth: ^6.1.4

View File

@@ -9,6 +9,7 @@ import 'domain/usecases/create_permanent_order_usecase.dart';
import 'domain/usecases/create_recurring_order_usecase.dart'; import 'domain/usecases/create_recurring_order_usecase.dart';
import 'domain/usecases/create_rapid_order_usecase.dart'; import 'domain/usecases/create_rapid_order_usecase.dart';
import 'domain/usecases/get_order_details_for_reorder_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 'domain/usecases/transcribe_rapid_order_usecase.dart';
import 'presentation/blocs/index.dart'; import 'presentation/blocs/index.dart';
import 'presentation/pages/create_order_page.dart'; import 'presentation/pages/create_order_page.dart';
@@ -24,7 +25,7 @@ import 'presentation/pages/recurring_order_page.dart';
/// presentation layer BLoCs. /// presentation layer BLoCs.
class ClientCreateOrderModule extends Module { class ClientCreateOrderModule extends Module {
@override @override
List<Module> get imports => <Module>[DataConnectModule(), CoreModule()]; List<Module> get imports => <Module>[DataConnectModule()];
@override @override
void binds(Injector i) { void binds(Injector i) {
@@ -39,13 +40,14 @@ class ClientCreateOrderModule extends Module {
i.addLazySingleton(CreateRecurringOrderUseCase.new); i.addLazySingleton(CreateRecurringOrderUseCase.new);
i.addLazySingleton(CreateRapidOrderUseCase.new); i.addLazySingleton(CreateRapidOrderUseCase.new);
i.addLazySingleton(TranscribeRapidOrderUseCase.new); i.addLazySingleton(TranscribeRapidOrderUseCase.new);
i.addLazySingleton(ParseRapidOrderTextToOrderUseCase.new);
i.addLazySingleton(GetOrderDetailsForReorderUseCase.new); i.addLazySingleton(GetOrderDetailsForReorderUseCase.new);
// BLoCs // BLoCs
i.add<RapidOrderBloc>( i.add<RapidOrderBloc>(
(Injector i) => RapidOrderBloc( (Injector i) => RapidOrderBloc(
i.get<CreateRapidOrderUseCase>(),
i.get<TranscribeRapidOrderUseCase>(), i.get<TranscribeRapidOrderUseCase>(),
i.get<ParseRapidOrderTextToOrderUseCase>(),
i.get<AudioRecorderService>(), i.get<AudioRecorderService>(),
), ),
); );

View File

@@ -1,6 +1,5 @@
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain; import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/client_create_order_repository_interface.dart'; import '../../domain/repositories/client_create_order_repository_interface.dart';
@@ -14,14 +13,10 @@ import '../../domain/repositories/client_create_order_repository_interface.dart'
/// on delegation and data mapping, without business logic. /// on delegation and data mapping, without business logic.
class ClientCreateOrderRepositoryImpl class ClientCreateOrderRepositoryImpl
implements ClientCreateOrderRepositoryInterface { implements ClientCreateOrderRepositoryInterface {
ClientCreateOrderRepositoryImpl({ ClientCreateOrderRepositoryImpl({required dc.DataConnectService service})
required dc.DataConnectService service, : _service = service;
required RapidOrderService rapidOrderService,
}) : _service = service,
_rapidOrderService = rapidOrderService;
final dc.DataConnectService _service; final dc.DataConnectService _service;
final RapidOrderService _rapidOrderService;
@override @override
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async { Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
@@ -372,6 +367,44 @@ class ClientCreateOrderRepositoryImpl
throw UnimplementedError('Rapid order IA is not connected yet.'); 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 @override
Future<String> transcribeRapidOrder(String audioPath) async { Future<String> transcribeRapidOrder(String audioPath) async {
final RapidOrderTranscriptionResponse response = await _rapidOrderService final RapidOrderTranscriptionResponse response = await _rapidOrderService

View File

@@ -29,6 +29,11 @@ abstract interface class ClientCreateOrderRepositoryInterface {
/// [audioPath] is the local path to the recorded audio file. /// [audioPath] is the local path to the recorded audio file.
Future<String> transcribeRapidOrder(String audioPath); 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. /// Reorders an existing staffing order with a new date.
/// ///
/// [previousOrderId] is the ID of the order to reorder. /// [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( Future<void> _loadManagersForHub(String hubId) async {
String hubId,
) async {
final List<OneTimeOrderManagerOption>? managers = final List<OneTimeOrderManagerOption>? managers =
await handleErrorWithResult( await handleErrorWithResult(
action: () async { action: () async {
@@ -163,7 +161,9 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
.toList(); .toList();
}, },
onError: (_) { onError: (_) {
add(const OneTimeOrderManagersLoaded(<OneTimeOrderManagerOption>[])); add(
const OneTimeOrderManagersLoaded(<OneTimeOrderManagerOption>[]),
);
}, },
); );
@@ -172,7 +172,6 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
} }
} }
Future<void> _onVendorsLoaded( Future<void> _onVendorsLoaded(
OneTimeOrderVendorsLoaded event, OneTimeOrderVendorsLoaded event,
Emitter<OneTimeOrderState> emit, Emitter<OneTimeOrderState> emit,
@@ -216,7 +215,6 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
} }
} }
void _onHubChanged( void _onHubChanged(
OneTimeOrderHubChanged event, OneTimeOrderHubChanged event,
Emitter<OneTimeOrderState> emit, Emitter<OneTimeOrderState> emit,
@@ -239,7 +237,6 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
emit(state.copyWith(managers: event.managers)); emit(state.copyWith(managers: event.managers));
} }
void _onEventNameChanged( void _onEventNameChanged(
OneTimeOrderEventNameChanged event, OneTimeOrderEventNameChanged event,
Emitter<OneTimeOrderState> emit, Emitter<OneTimeOrderState> emit,
@@ -349,6 +346,45 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
final DateTime? startDate = data['startDate'] as DateTime?; final DateTime? startDate = data['startDate'] as DateTime?;
final String? orderId = data['orderId']?.toString(); 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())); emit(state.copyWith(eventName: title, date: startDate ?? DateTime.now()));
if (orderId == null || orderId.isEmpty) return; if (orderId == null || orderId.isEmpty) return;

View File

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

View File

@@ -1,8 +1,8 @@
import 'package:client_create_order/src/domain/arguments/rapid_order_arguments.dart'; import 'package:client_create_order/src/domain/usecases/parse_rapid_order_usecase.dart';
import 'package:client_create_order/src/domain/usecases/create_rapid_order_usecase.dart';
import 'package:client_create_order/src/domain/usecases/transcribe_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:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'rapid_order_event.dart'; import 'rapid_order_event.dart';
import 'rapid_order_state.dart'; import 'rapid_order_state.dart';
@@ -11,8 +11,8 @@ import 'rapid_order_state.dart';
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
with BlocErrorHandler<RapidOrderState> { with BlocErrorHandler<RapidOrderState> {
RapidOrderBloc( RapidOrderBloc(
this._createRapidOrderUseCase,
this._transcribeRapidOrderUseCase, this._transcribeRapidOrderUseCase,
this._parseRapidOrderUseCase,
this._audioRecorderService, this._audioRecorderService,
) : super( ) : super(
const RapidOrderInitial( const RapidOrderInitial(
@@ -28,8 +28,8 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
on<RapidOrderSubmitted>(_onSubmitted); on<RapidOrderSubmitted>(_onSubmitted);
on<RapidOrderExampleSelected>(_onExampleSelected); on<RapidOrderExampleSelected>(_onExampleSelected);
} }
final CreateRapidOrderUseCase _createRapidOrderUseCase;
final TranscribeRapidOrderUseCase _transcribeRapidOrderUseCase; final TranscribeRapidOrderUseCase _transcribeRapidOrderUseCase;
final ParseRapidOrderTextToOrderUseCase _parseRapidOrderUseCase;
final AudioRecorderService _audioRecorderService; final AudioRecorderService _audioRecorderService;
void _onMessageChanged( void _onMessageChanged(
@@ -47,39 +47,21 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
) async { ) async {
if (state is RapidOrderInitial) { if (state is RapidOrderInitial) {
final RapidOrderInitial currentState = state as RapidOrderInitial; final RapidOrderInitial currentState = state as RapidOrderInitial;
final bool alreadyListening = currentState.isListening; final bool newListeningState = !currentState.isListening;
if (!alreadyListening) { emit(currentState.copyWith(isListening: newListeningState));
// Start recording
await handleError(
emit: (RapidOrderState s) => emit(s),
action: () async {
await _audioRecorderService.startRecording();
emit(currentState.copyWith(isListening: true));
},
onError: (String errorKey) => RapidOrderFailure(errorKey),
);
} else {
// Stop and transcribe
emit(currentState.copyWith(isListening: false));
await handleError( // Simulate voice recognition
emit: (RapidOrderState s) => emit(s), if (newListeningState) {
action: () async { await Future<void>.delayed(const Duration(seconds: 2));
final String? path = await _audioRecorderService.stopRecording(); if (state is RapidOrderInitial) {
if (path != null) { emit(
final String transcript = await _transcribeRapidOrderUseCase( (state as RapidOrderInitial).copyWith(
path, message: 'Need 2 servers for a banquet right now.',
); isListening: false,
if (state is RapidOrderInitial) { ),
emit( );
(state as RapidOrderInitial).copyWith(message: transcript), }
);
}
}
},
onError: (String errorKey) => RapidOrderFailure(errorKey),
);
} }
} }
} }
@@ -96,10 +78,8 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
await handleError( await handleError(
emit: emit.call, emit: emit.call,
action: () async { action: () async {
await _createRapidOrderUseCase( final OneTimeOrder order = await _parseRapidOrderUseCase(message);
RapidOrderArguments(description: message), emit(RapidOrderParsed(order));
);
emit(const RapidOrderSuccess());
}, },
onError: (String errorKey) => RapidOrderFailure(errorKey), onError: (String errorKey) => RapidOrderFailure(errorKey),
); );
@@ -116,3 +96,4 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState>
} }
} }
} }

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class RapidOrderState extends Equatable { abstract class RapidOrderState extends Equatable {
const RapidOrderState(); const RapidOrderState();
@@ -48,3 +49,11 @@ class RapidOrderFailure extends RapidOrderState {
@override @override
List<Object?> get props => <Object?>[error]; 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, : null,
hubManagers: state.managers.map(_mapManager).toList(), hubManagers: state.managers.map(_mapManager).toList(),
isValid: state.isValid, isValid: state.isValid,
title: state.isRapidDraft ? 'Rapid Order : Verify the order' : null,
onEventNameChanged: (String val) => onEventNameChanged: (String val) =>
bloc.add(OneTimeOrderEventNameChanged(val)), bloc.add(OneTimeOrderEventNameChanged(val)),
onVendorChanged: (Vendor val) => onVendorChanged: (Vendor val) =>

View File

@@ -72,6 +72,13 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
TextPosition(offset: _messageController.text.length), 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) { } else if (state is RapidOrderFailure) {
UiSnackbar.show( UiSnackbar.show(
context, context,

View File

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

View File

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

View File

@@ -186,7 +186,8 @@ client.createInvoice();
| dataconnect-deploy | Deploy schemas | | dataconnect-deploy | Deploy schemas |
| dataconnect-sql-migrate | Apply DB migrations | | dataconnect-sql-migrate | Apply DB migrations |
| dataconnect-generate-sdk | Generate SDK | | dataconnect-generate-sdk | Generate SDK |
| dataconnect-sync | Full backend update | | dataconnect-sync | Fast connector + SDK sync |
| dataconnect-sync-full | Full backend update |
| dataconnect-test | Test without breaking | | dataconnect-test | Test without breaking |
| dataconnect-seed | Insert seed data | | dataconnect-seed | Insert seed data |
| dataconnect-bootstrap-db | Create Cloud SQL | | dataconnect-bootstrap-db | Create Cloud SQL |
@@ -195,7 +196,7 @@ client.createInvoice();
## 9. Correct Backend Workflow ## 9. Correct Backend Workflow
### Production Flow ### Query/Connector Flow
```bash ```bash
make dataconnect-sync make dataconnect-sync
@@ -203,6 +204,19 @@ make dataconnect-sync
Steps: Steps:
1. Deploy connector
2. Generate SDK
---
### Full Schema Flow
```bash
make dataconnect-sync-full
```
Steps:
1. Deploy schema 1. Deploy schema
2. Run SQL migrations 2. Run SQL migrations
3. Generate SDK 3. Generate SDK

View File

@@ -186,7 +186,8 @@ client.createInvoice();
| dataconnect-deploy | Deploy schemas | | dataconnect-deploy | Deploy schemas |
| dataconnect-sql-migrate | Apply DB migrations | | dataconnect-sql-migrate | Apply DB migrations |
| dataconnect-generate-sdk | Generate SDK | | dataconnect-generate-sdk | Generate SDK |
| dataconnect-sync | Full backend update | | dataconnect-sync | Fast connector + SDK sync |
| dataconnect-sync-full | Full backend update |
| dataconnect-test | Test without breaking | | dataconnect-test | Test without breaking |
| dataconnect-seed | Insert seed data | | dataconnect-seed | Insert seed data |
| dataconnect-bootstrap-db | Create Cloud SQL | | dataconnect-bootstrap-db | Create Cloud SQL |
@@ -195,7 +196,7 @@ client.createInvoice();
## 9. Correct Backend Workflow ## 9. Correct Backend Workflow
### Production Flow ### Query/Connector Flow
```bash ```bash
make dataconnect-sync make dataconnect-sync
@@ -203,6 +204,19 @@ make dataconnect-sync
Steps: Steps:
1. Deploy connector
2. Generate SDK
---
### Full Schema Flow
```bash
make dataconnect-sync-full
```
Steps:
1. Deploy schema 1. Deploy schema
2. Run SQL migrations 2. Run SQL migrations
3. Generate SDK 3. Generate SDK

View File

@@ -2,11 +2,14 @@
# Usage examples: # Usage examples:
# make dataconnect-sync DC_ENV=dev # make dataconnect-sync DC_ENV=dev
# make dataconnect-sync-full DC_ENV=dev
# make dataconnect-seed DC_ENV=validation # make dataconnect-seed DC_ENV=validation
# make dataconnect-clean DC_ENV=validation # make dataconnect-clean DC_ENV=validation
# make dataconnect-generate-sdk DC_ENV=dev # make dataconnect-generate-sdk DC_ENV=dev
# #
DC_ENV ?= dev DC_ENV ?= dev
DC_LOCATION ?= us-central1
DC_CONNECTOR_ID ?= example
DC_SERVICE_DEV := krow-workforce-db DC_SERVICE_DEV := krow-workforce-db
DC_SERVICE_VALIDATION := krow-workforce-db-validation DC_SERVICE_VALIDATION := krow-workforce-db-validation
@@ -19,7 +22,7 @@ else
$(error Invalid DC_ENV '$(DC_ENV)'. Use DC_ENV=dev or DC_ENV=validation) $(error Invalid DC_ENV '$(DC_ENV)'. Use DC_ENV=dev or DC_ENV=validation)
endif endif
.PHONY: dataconnect-enable-apis dataconnect-init dataconnect-deploy dataconnect-sql-migrate dataconnect-generate-sdk dataconnect-sync dataconnect-bootstrap-db check-gcloud-beta dataconnect-clean dataconnect-bootstrap-validation-db dataconnect-file dataconnect-file-validation dataconnect-file-dev dataconnect-seed dataconnect-test .PHONY: dataconnect-enable-apis dataconnect-init dataconnect-deploy dataconnect-sql-migrate dataconnect-generate-sdk dataconnect-sync dataconnect-sync-full dataconnect-bootstrap-db check-gcloud-beta dataconnect-clean dataconnect-bootstrap-validation-db dataconnect-file dataconnect-file-validation dataconnect-file-dev dataconnect-seed dataconnect-test
#creation dataconnect file #creation dataconnect file
dataconnect-file: dataconnect-file:
@@ -79,15 +82,23 @@ dataconnect-generate-sdk: dataconnect-file
@firebase dataconnect:sdk:generate --project=$(FIREBASE_ALIAS) @firebase dataconnect:sdk:generate --project=$(FIREBASE_ALIAS)
@echo "✅ Data Connect SDK generation completed for [$(DC_ENV)]." @echo "✅ Data Connect SDK generation completed for [$(DC_ENV)]."
# Unified backend schema update workflow (schema -> deploy -> SDK) # Fast sync workflow for connector/query changes (deploy connector -> SDK)
dataconnect-sync: dataconnect-file dataconnect-sync: dataconnect-file
@echo "--> [1/3] Deploying Data Connect [$(DC_SERVICE)]..." @echo "--> [1/2] Deploying Data Connect connector [$(DC_SERVICE):$(DC_CONNECTOR_ID)]..."
@firebase deploy --only dataconnect:$(DC_SERVICE) --project=$(FIREBASE_ALIAS) @firebase deploy --only dataconnect:$(DC_SERVICE):$(DC_CONNECTOR_ID) --project=$(FIREBASE_ALIAS) --force
@echo "--> [2/2] Regenerating SDK [$(DC_SERVICE)]..."
@firebase dataconnect:sdk:generate --project=$(FIREBASE_ALIAS)
@echo "✅ Data Connect connector + SDK sync completed for [$(DC_ENV)]."
# Full backend schema update workflow (schema -> migrate -> SDK)
dataconnect-sync-full: dataconnect-file
@echo "--> [1/3] Deploying full Data Connect service [$(DC_SERVICE)]..."
@firebase deploy --only dataconnect:$(DC_SERVICE) --project=$(FIREBASE_ALIAS) --force
@echo "--> [2/3] Applying SQL migrations [$(DC_SERVICE)]..." @echo "--> [2/3] Applying SQL migrations [$(DC_SERVICE)]..."
@firebase dataconnect:sql:migrate --project=$(FIREBASE_ALIAS) @firebase dataconnect:sql:migrate --project=$(FIREBASE_ALIAS) --service $(DC_SERVICE) --location $(DC_LOCATION) --force
@echo "--> [3/3] Regenerating SDK [$(DC_SERVICE)]..." @echo "--> [3/3] Regenerating SDK [$(DC_SERVICE)]..."
@firebase dataconnect:sdk:generate --project=$(FIREBASE_ALIAS) @firebase dataconnect:sdk:generate --project=$(FIREBASE_ALIAS)
@echo "✅ Data Connect SQL, deploy, and SDK generation [$(ENV)]." @echo "✅ Data Connect full schema sync completed for [$(DC_ENV)]."
# Execute seed in Firebase Data Connect # Execute seed in Firebase Data Connect
dataconnect-seed: dataconnect-file dataconnect-seed: dataconnect-file