Merge branch '208-p0-auth-05-get-started-screen' into rapid_order_client

This commit is contained in:
José Salazar
2026-01-23 14:31:25 -05:00
43 changed files with 3226 additions and 387 deletions

View File

@@ -8,7 +8,7 @@ import 'client_create_order_state.dart';
class ClientCreateOrderBloc
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
ClientCreateOrderBloc(this._getOrderTypesUseCase)
: super(const ClientCreateOrderInitial()) {
: super(const ClientCreateOrderInitial()) {
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
}
final GetOrderTypesUseCase _getOrderTypesUseCase;

View File

@@ -8,16 +8,71 @@ 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()) {
: super(OneTimeOrderState.initial()) {
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
on<OneTimeOrderVendorChanged>(_onVendorChanged);
on<OneTimeOrderDateChanged>(_onDateChanged);
on<OneTimeOrderLocationChanged>(_onLocationChanged);
on<OneTimeOrderPositionAdded>(_onPositionAdded);
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
on<OneTimeOrderSubmitted>(_onSubmitted);
// Initial load of mock vendors
add(
const OneTimeOrderVendorsLoaded(<Vendor>[
Vendor(
id: 'v1',
name: 'Elite Staffing',
rates: <String, double>{
'Server': 25.0,
'Bartender': 30.0,
'Cook': 28.0,
'Busser': 18.0,
'Host': 20.0,
'Barista': 22.0,
'Dishwasher': 17.0,
'Event Staff': 19.0,
},
),
Vendor(
id: 'v2',
name: 'Premier Workforce',
rates: <String, double>{
'Server': 22.0,
'Bartender': 28.0,
'Cook': 25.0,
'Busser': 16.0,
'Host': 18.0,
'Barista': 20.0,
'Dishwasher': 15.0,
'Event Staff': 18.0,
},
),
]),
);
}
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
void _onVendorsLoaded(
OneTimeOrderVendorsLoaded event,
Emitter<OneTimeOrderState> emit,
) {
emit(
state.copyWith(
vendors: event.vendors,
selectedVendor: event.vendors.isNotEmpty ? event.vendors.first : null,
),
);
}
void _onVendorChanged(
OneTimeOrderVendorChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(state.copyWith(selectedVendor: event.vendor));
}
void _onDateChanged(
OneTimeOrderDateChanged event,
Emitter<OneTimeOrderState> emit,
@@ -37,13 +92,14 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
Emitter<OneTimeOrderState> emit,
) {
final List<OneTimeOrderPosition> newPositions =
List<OneTimeOrderPosition>.from(state.positions)
..add(const OneTimeOrderPosition(
List<OneTimeOrderPosition>.from(state.positions)..add(
const OneTimeOrderPosition(
role: '',
count: 1,
startTime: '',
endTime: '',
));
startTime: '09:00',
endTime: '17:00',
),
);
emit(state.copyWith(positions: newPositions));
}
@@ -79,14 +135,17 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
date: state.date,
location: state.location,
positions: state.positions,
// In a real app, we'd pass the vendorId here
);
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
emit(state.copyWith(status: OneTimeOrderStatus.success));
} catch (e) {
emit(state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: e.toString(),
));
emit(
state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: e.toString(),
),
);
}
}
}

View File

@@ -8,6 +8,22 @@ abstract class OneTimeOrderEvent extends Equatable {
List<Object?> get props => <Object?>[];
}
class OneTimeOrderVendorsLoaded extends OneTimeOrderEvent {
const OneTimeOrderVendorsLoaded(this.vendors);
final List<Vendor> vendors;
@override
List<Object?> get props => <Object?>[vendors];
}
class OneTimeOrderVendorChanged extends OneTimeOrderEvent {
const OneTimeOrderVendorChanged(this.vendor);
final Vendor vendor;
@override
List<Object?> get props => <Object?>[vendor];
}
class OneTimeOrderDateChanged extends OneTimeOrderEvent {
const OneTimeOrderDateChanged(this.date);
final DateTime date;

View File

@@ -10,6 +10,8 @@ class OneTimeOrderState extends Equatable {
required this.positions,
this.status = OneTimeOrderStatus.initial,
this.errorMessage,
this.vendors = const <Vendor>[],
this.selectedVendor,
});
factory OneTimeOrderState.initial() {
@@ -17,13 +19,9 @@ class OneTimeOrderState extends Equatable {
date: DateTime.now(),
location: '',
positions: const <OneTimeOrderPosition>[
OneTimeOrderPosition(
role: '',
count: 1,
startTime: '',
endTime: '',
),
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
],
vendors: const <Vendor>[],
);
}
final DateTime date;
@@ -31,6 +29,8 @@ class OneTimeOrderState extends Equatable {
final List<OneTimeOrderPosition> positions;
final OneTimeOrderStatus status;
final String? errorMessage;
final List<Vendor> vendors;
final Vendor? selectedVendor;
OneTimeOrderState copyWith({
DateTime? date,
@@ -38,6 +38,8 @@ class OneTimeOrderState extends Equatable {
List<OneTimeOrderPosition>? positions,
OneTimeOrderStatus? status,
String? errorMessage,
List<Vendor>? vendors,
Vendor? selectedVendor,
}) {
return OneTimeOrderState(
date: date ?? this.date,
@@ -45,15 +47,19 @@ class OneTimeOrderState extends Equatable {
positions: positions ?? this.positions,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
vendors: vendors ?? this.vendors,
selectedVendor: selectedVendor ?? this.selectedVendor,
);
}
@override
List<Object?> get props => <Object?>[
date,
location,
positions,
status,
errorMessage,
];
date,
location,
positions,
status,
errorMessage,
vendors,
selectedVendor,
];
}

View File

@@ -7,15 +7,15 @@ 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"',
],
),
) {
: 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);
@@ -68,7 +68,8 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
try {
await _createRapidOrderUseCase(
RapidOrderArguments(description: message));
RapidOrderArguments(description: message),
);
emit(const RapidOrderSuccess());
} catch (e) {
emit(RapidOrderFailure(e.toString()));

View File

@@ -17,8 +17,9 @@ class ClientCreateOrderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<ClientCreateOrderBloc>(
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
..add(const ClientCreateOrderTypesRequested()),
create: (BuildContext context) =>
Modular.get<ClientCreateOrderBloc>()
..add(const ClientCreateOrderTypesRequested()),
child: const CreateOrderView(),
);
}

View File

@@ -65,59 +65,58 @@ class CreateOrderView extends StatelessWidget {
),
),
Expanded(
child:
BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
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);
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);
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.pushRapidOrder();
break;
case 'one-time':
Modular.to.pushOneTimeOrder();
break;
case 'recurring':
Modular.to.pushRecurringOrder();
break;
case 'permanent':
Modular.to.pushPermanentOrder();
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.pushRapidOrder();
break;
case 'one-time':
Modular.to.pushOneTimeOrder();
break;
case 'recurring':
Modular.to.pushRecurringOrder();
break;
case 'permanent':
Modular.to.pushPermanentOrder();
break;
}
},
);
},
);
},
);
}
return const Center(child: CircularProgressIndicator());
},
}
return const Center(child: CircularProgressIndicator());
},
),
),
],

View File

@@ -54,9 +54,7 @@ class OneTimeOrderHeader extends StatelessWidget {
children: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(
color: UiColors.white,
),
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,

View File

@@ -19,6 +19,7 @@ class OneTimeOrderPositionCard extends StatelessWidget {
required this.startLabel,
required this.endLabel,
required this.lunchLabel,
this.vendor,
super.key,
});
@@ -55,6 +56,9 @@ class OneTimeOrderPositionCard extends StatelessWidget {
/// Label for the lunch break.
final String lunchLabel;
/// The current selected vendor to determine rates.
final Vendor? vendor;
@override
Widget build(BuildContext context) {
return Container(
@@ -99,8 +103,10 @@ class OneTimeOrderPositionCard extends StatelessWidget {
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
hint:
Text(roleLabel, style: UiTypography.body2r.textPlaceholder),
hint: Text(
roleLabel,
style: UiTypography.body2r.textPlaceholder,
),
value: position.role.isEmpty ? null : position.role,
icon: const Icon(
UiIcons.chevronDown,
@@ -112,26 +118,26 @@ class OneTimeOrderPositionCard extends StatelessWidget {
onUpdated(position.copyWith(role: val));
}
},
items: <String>[
'Server',
'Bartender',
'Cook',
'Busser',
'Host',
'Barista',
'Dishwasher',
'Event Staff'
].map((String role) {
// Mock rates for UI matching
final int rate = _getMockRate(role);
return DropdownMenuItem<String>(
value: role,
child: Text(
'$role - \$$rate/hr',
style: UiTypography.body2r.textPrimary,
),
);
}).toList(),
items:
<String>{
...(vendor?.rates.keys ?? <String>[]),
if (position.role.isNotEmpty &&
!(vendor?.rates.keys.contains(position.role) ??
false))
position.role,
}.map((String role) {
final double? rate = vendor?.rates[role];
final String label = rate == null
? role
: '$role - \$${rate.toStringAsFixed(0)}/hr';
return DropdownMenuItem<String>(
value: role,
child: Text(
label,
style: UiTypography.body2r.textPrimary,
),
);
}).toList(),
),
),
),
@@ -153,7 +159,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
);
if (picked != null && context.mounted) {
onUpdated(
position.copyWith(startTime: picked.format(context)));
position.copyWith(startTime: picked.format(context)),
);
}
},
),
@@ -172,7 +179,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
);
if (picked != null && context.mounted) {
onUpdated(
position.copyWith(endTime: picked.format(context)));
position.copyWith(endTime: picked.format(context)),
);
}
},
),
@@ -198,10 +206,13 @@ class OneTimeOrderPositionCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
GestureDetector(
onTap: () => onUpdated(position.copyWith(
count: (position.count > 1)
? position.count - 1
: 1)),
onTap: () {
if (position.count > 1) {
onUpdated(
position.copyWith(count: position.count - 1),
);
}
},
child: const Icon(UiIcons.minus, size: 12),
),
Text(
@@ -209,8 +220,11 @@ class OneTimeOrderPositionCard extends StatelessWidget {
style: UiTypography.body2b.textPrimary,
),
GestureDetector(
onTap: () => onUpdated(
position.copyWith(count: position.count + 1)),
onTap: () {
onUpdated(
position.copyWith(count: position.count + 1),
);
},
child: const Icon(UiIcons.add, size: 12),
),
],
@@ -223,74 +237,12 @@ class OneTimeOrderPositionCard extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
// Optional Location Override
if (position.location == null)
GestureDetector(
onTap: () => onUpdated(position.copyWith(location: '')),
child: Row(
children: <Widget>[
const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary),
const SizedBox(width: UiConstants.space1),
Text(
t.client_create_order.one_time.different_location,
style: UiTypography.footnote1m.copyWith(
color: UiColors.primary,
),
),
],
),
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: <Widget>[
const Icon(UiIcons.mapPin,
size: 14, color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Text(
t.client_create_order.one_time
.different_location_title,
style: UiTypography.footnote1m.textSecondary,
),
],
),
GestureDetector(
onTap: () => onUpdated(position.copyWith(location: null)),
child: const Icon(
UiIcons.close,
size: 14,
color: UiColors.destructive,
),
),
],
),
const SizedBox(height: UiConstants.space2),
_PositionLocationInput(
value: position.location ?? '',
onChanged: (String val) =>
onUpdated(position.copyWith(location: val)),
hintText:
t.client_create_order.one_time.different_location_hint,
),
],
),
const SizedBox(height: UiConstants.space3),
// Lunch Break
Text(
lunchLabel,
style: UiTypography.footnote2r.textSecondary,
),
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space1),
Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 44,
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
@@ -309,43 +261,15 @@ class OneTimeOrderPositionCard extends StatelessWidget {
onUpdated(position.copyWith(lunchBreak: val));
}
},
items: <DropdownMenuItem<int>>[
DropdownMenuItem<int>(
value: 0,
child: Text(t.client_create_order.one_time.no_break,
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 10,
items: <int>[0, 15, 30, 45, 60].map((int mins) {
return DropdownMenuItem<int>(
value: mins,
child: Text(
'10 ${t.client_create_order.one_time.paid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 15,
child: Text(
'15 ${t.client_create_order.one_time.paid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 30,
child: Text(
'30 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 45,
child: Text(
'45 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
DropdownMenuItem<int>(
value: 60,
child: Text(
'60 ${t.client_create_order.one_time.unpaid_break}',
style: UiTypography.body2r.textPrimary),
),
],
mins == 0 ? 'No Break' : '$mins mins',
style: UiTypography.body2r.textPrimary,
),
);
}).toList(),
),
),
),
@@ -360,83 +284,37 @@ class OneTimeOrderPositionCard extends StatelessWidget {
required String value,
required VoidCallback onTap,
}) {
return UiTextField(
label: label,
controller: TextEditingController(text: value),
readOnly: true,
onTap: onTap,
hintText: '--:--',
);
}
int _getMockRate(String role) {
switch (role) {
case 'Server':
return 18;
case 'Bartender':
return 22;
case 'Cook':
return 20;
case 'Busser':
return 16;
case 'Host':
return 17;
case 'Barista':
return 16;
case 'Dishwasher':
return 15;
case 'Event Staff':
return 20;
default:
return 15;
}
}
}
class _PositionLocationInput extends StatefulWidget {
const _PositionLocationInput({
required this.value,
required this.hintText,
required this.onChanged,
});
final String value;
final String hintText;
final ValueChanged<String> onChanged;
@override
State<_PositionLocationInput> createState() => _PositionLocationInputState();
}
class _PositionLocationInputState extends State<_PositionLocationInput> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.value);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(_PositionLocationInput oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != _controller.text) {
_controller.text = widget.value;
}
}
@override
Widget build(BuildContext context) {
return UiTextField(
controller: _controller,
onChanged: widget.onChanged,
hintText: widget.hintText,
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space1),
GestureDetector(
onTap: onTap,
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
decoration: BoxDecoration(
borderRadius: UiConstants.radiusSm,
border: Border.all(color: UiColors.border),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
value.isEmpty ? '--:--' : value,
style: UiTypography.body2r.textPrimary,
),
const Icon(
UiIcons.clock,
size: 14,
color: UiColors.iconSecondary,
),
],
),
),
),
],
);
}
}

View File

@@ -27,14 +27,28 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
children: <Widget>[
Text(title, style: UiTypography.headline4m.textPrimary),
if (actionLabel != null && onAction != null)
UiButton.text(
TextButton(
onPressed: onAction,
leadingIcon: UiIcons.add,
text: actionLabel!,
iconSize: 16,
style: TextButton.styleFrom(
minimumSize: const Size(0, 24),
maximumSize: const Size(0, 24),
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(UiIcons.add, size: 16, color: Color(0xFF0032A0)),
const SizedBox(width: UiConstants.space2),
Text(
actionLabel!,
style: const TextStyle(
color: Color(0xFF0032A0),
fontSize: 14,
fontWeight:
FontWeight.w500, // Added to match typical button text
),
),
],
),
),
],

View File

@@ -35,6 +35,47 @@ class OneTimeOrderView extends StatelessWidget {
);
}
if (state.vendors.isEmpty &&
state.status != OneTimeOrderStatus.loading) {
return Scaffold(
backgroundColor: UiColors.bgPrimary,
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: () => Modular.to.pop(),
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
return Scaffold(
backgroundColor: UiColors.bgPrimary,
body: Column(
@@ -58,8 +99,9 @@ class OneTimeOrderView extends StatelessWidget {
? labels.creating
: labels.create_order,
isLoading: state.status == OneTimeOrderStatus.loading,
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
.add(const OneTimeOrderSubmitted()),
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(const OneTimeOrderSubmitted()),
),
],
),
@@ -87,37 +129,78 @@ class _OneTimeOrderForm extends StatelessWidget {
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: state.selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderVendorChanged(vendor));
}
},
items: state.vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderDatePicker(
label: labels.date_label,
value: state.date,
onChanged: (DateTime date) =>
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderDateChanged(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)),
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()),
onAction: () => BlocProvider.of<OneTimeOrderBloc>(
context,
).add(const OneTimeOrderPositionAdded()),
),
const SizedBox(height: UiConstants.space3),
// Positions List
...state.positions
.asMap()
.entries
.map((MapEntry<int, OneTimeOrderPosition> entry) {
...state.positions.asMap().entries.map((
MapEntry<int, OneTimeOrderPosition> entry,
) {
final int index = entry.key;
final OneTimeOrderPosition position = entry.value;
return Padding(
@@ -132,14 +215,16 @@ class _OneTimeOrderForm extends StatelessWidget {
startLabel: labels.start_label,
endLabel: labels.end_label,
lunchLabel: labels.lunch_break_label,
vendor: state.selectedVendor,
onUpdated: (OneTimeOrderPosition updated) {
BlocProvider.of<OneTimeOrderBloc>(context).add(
OneTimeOrderPositionUpdated(index, updated),
);
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderPositionUpdated(index, updated));
},
onRemoved: () {
BlocProvider.of<OneTimeOrderBloc>(context)
.add(OneTimeOrderPositionRemoved(index));
BlocProvider.of<OneTimeOrderBloc>(
context,
).add(OneTimeOrderPositionRemoved(index));
},
),
);

View File

@@ -73,16 +73,14 @@ class OrderTypeCard extends StatelessWidget {
),
child: Icon(icon, color: iconColor, size: 24),
),
Text(
title,
style: UiTypography.body2b.copyWith(color: textColor),
),
Text(title, style: UiTypography.body2b.copyWith(color: textColor)),
const SizedBox(height: UiConstants.space1),
Expanded(
child: Text(
description,
style:
UiTypography.footnote1r.copyWith(color: descriptionColor),
style: UiTypography.footnote1r.copyWith(
color: descriptionColor,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),

View File

@@ -47,10 +47,7 @@ class RapidOrderExampleCard extends StatelessWidget {
text: TextSpan(
style: UiTypography.body2r.textPrimary,
children: <InlineSpan>[
TextSpan(
text: label,
style: UiTypography.body2b.textPrimary,
),
TextSpan(text: label, style: UiTypography.body2b.textPrimary),
TextSpan(text: ' $example'),
],
),

View File

@@ -74,11 +74,7 @@ class RapidOrderHeader extends StatelessWidget {
children: <Widget>[
Row(
children: <Widget>[
const Icon(
UiIcons.zap,
color: UiColors.accent,
size: 18,
),
const Icon(UiIcons.zap, color: UiColors.accent, size: 18),
const SizedBox(width: UiConstants.space2),
Text(
title,

View File

@@ -42,8 +42,9 @@ class RapidOrderSuccessView extends StatelessWidget {
child: SafeArea(
child: Center(
child: Container(
margin:
const EdgeInsets.symmetric(horizontal: UiConstants.space10),
margin: const EdgeInsets.symmetric(
horizontal: UiConstants.space10,
),
padding: const EdgeInsets.all(UiConstants.space8),
decoration: BoxDecoration(
color: UiColors.white,
@@ -75,10 +76,7 @@ class RapidOrderSuccessView extends StatelessWidget {
),
),
const SizedBox(height: UiConstants.space6),
Text(
title,
style: UiTypography.headline1m.textPrimary,
),
Text(title, style: UiTypography.headline1m.textPrimary),
const SizedBox(height: UiConstants.space3),
Text(
message,

View File

@@ -153,27 +153,27 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
// Examples
if (initialState != null)
...initialState.examples
.asMap()
.entries
.map((MapEntry<int, String> entry) {
...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),
bottom: UiConstants.space2,
),
child: RapidOrderExampleCard(
example: example,
isHighlighted: isHighlighted,
label: labels.example,
onTap: () =>
BlocProvider.of<RapidOrderBloc>(
context)
.add(
RapidOrderExampleSelected(example),
),
context,
).add(
RapidOrderExampleSelected(example),
),
),
);
}),
@@ -184,9 +184,9 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
controller: _messageController,
maxLines: 4,
onChanged: (String value) {
BlocProvider.of<RapidOrderBloc>(context).add(
RapidOrderMessageChanged(value),
);
BlocProvider.of<RapidOrderBloc>(
context,
).add(RapidOrderMessageChanged(value));
},
hintText: labels.hint,
),
@@ -197,7 +197,8 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
labels: labels,
isSubmitting: isSubmitting,
isListening: initialState?.isListening ?? false,
isMessageEmpty: initialState != null &&
isMessageEmpty:
initialState != null &&
initialState.message.trim().isEmpty,
),
],
@@ -242,11 +243,7 @@ class _AnimatedZapIcon extends StatelessWidget {
),
],
),
child: const Icon(
UiIcons.zap,
color: UiColors.white,
size: 32,
),
child: const Icon(UiIcons.zap, color: UiColors.white, size: 32),
);
}
}
@@ -271,9 +268,9 @@ class _RapidOrderActions extends StatelessWidget {
child: UiButton.secondary(
text: isListening ? labels.listening : labels.speak,
leadingIcon: UiIcons.bell, // Placeholder for mic
onPressed: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderVoiceToggled(),
),
onPressed: () => BlocProvider.of<RapidOrderBloc>(
context,
).add(const RapidOrderVoiceToggled()),
style: OutlinedButton.styleFrom(
backgroundColor: isListening
? UiColors.destructive.withValues(alpha: 0.05)
@@ -291,9 +288,9 @@ class _RapidOrderActions extends StatelessWidget {
trailingIcon: UiIcons.arrowRight,
onPressed: isSubmitting || isMessageEmpty
? null
: () => BlocProvider.of<RapidOrderBloc>(context).add(
const RapidOrderSubmitted(),
),
: () => BlocProvider.of<RapidOrderBloc>(
context,
).add(const RapidOrderSubmitted()),
),
),
],