Add vendor selection and rates to order creation flow
Introduces a Vendor entity and integrates vendor selection into the one-time order creation and order editing flows. Vendor-specific rates are now displayed and used for position roles, and UI components have been updated to allow users to select a vendor and see corresponding rates. Mock vendor data is used for demonstration purposes.
This commit is contained in:
@@ -19,6 +19,7 @@ export 'src/entities/business/business_setting.dart';
|
||||
export 'src/entities/business/hub.dart';
|
||||
export 'src/entities/business/hub_department.dart';
|
||||
export 'src/entities/business/biz_contract.dart';
|
||||
export 'src/entities/business/vendor.dart';
|
||||
|
||||
// Events & Shifts
|
||||
export 'src/entities/events/event.dart';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a staffing vendor.
|
||||
class Vendor extends Equatable {
|
||||
const Vendor({required this.id, required this.name, required this.rates});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
/// A map of role names to hourly rates.
|
||||
final Map<String, double> rates;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, name, rates];
|
||||
}
|
||||
@@ -9,15 +9,70 @@ import 'one_time_order_state.dart';
|
||||
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
|
||||
: 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,
|
||||
@@ -41,8 +96,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||
const OneTimeOrderPosition(
|
||||
role: '',
|
||||
count: 1,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
startTime: '09:00',
|
||||
endTime: '17:00',
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(positions: newPositions));
|
||||
@@ -80,6 +135,7 @@ 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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
@@ -19,6 +21,7 @@ class OneTimeOrderState extends Equatable {
|
||||
positions: const <OneTimeOrderPosition>[
|
||||
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
|
||||
],
|
||||
vendors: const <Vendor>[],
|
||||
);
|
||||
}
|
||||
final DateTime date;
|
||||
@@ -26,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,
|
||||
@@ -33,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,
|
||||
@@ -40,6 +47,8 @@ class OneTimeOrderState extends Equatable {
|
||||
positions: positions ?? this.positions,
|
||||
status: status ?? this.status,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
vendors: vendors ?? this.vendors,
|
||||
selectedVendor: selectedVendor ?? this.selectedVendor,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,5 +59,7 @@ class OneTimeOrderState extends Equatable {
|
||||
positions,
|
||||
status,
|
||||
errorMessage,
|
||||
vendors,
|
||||
selectedVendor,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -115,22 +119,21 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
items:
|
||||
<String>[
|
||||
'Server',
|
||||
'Bartender',
|
||||
'Cook',
|
||||
'Busser',
|
||||
'Host',
|
||||
'Barista',
|
||||
'Dishwasher',
|
||||
'Event Staff',
|
||||
].map((String role) {
|
||||
// Mock rates for UI matching
|
||||
final int rate = _getMockRate(role);
|
||||
<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(
|
||||
'$role - \$$rate/hr',
|
||||
label,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
);
|
||||
@@ -203,13 +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(
|
||||
@@ -217,9 +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),
|
||||
),
|
||||
],
|
||||
@@ -232,76 +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),
|
||||
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),
|
||||
@@ -320,50 +261,15 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
||||
onUpdated(position.copyWith(lunchBreak: val));
|
||||
}
|
||||
},
|
||||
items: <DropdownMenuItem<int>>[
|
||||
DropdownMenuItem<int>(
|
||||
value: 0,
|
||||
items: <int>[0, 15, 30, 45, 60].map((int mins) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: mins,
|
||||
child: Text(
|
||||
t.client_create_order.one_time.no_break,
|
||||
mins == 0 ? 'No Break' : '$mins mins',
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<int>(
|
||||
value: 10,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -378,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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -88,6 +129,47 @@ 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,
|
||||
@@ -133,6 +215,7 @@ 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,
|
||||
|
||||
@@ -643,6 +643,9 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
|
||||
late List<Map<String, dynamic>> _positions;
|
||||
|
||||
List<Vendor> _vendors = const <Vendor>[];
|
||||
Vendor? _selectedVendor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -661,6 +664,39 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
'location': null,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock vendors initialization
|
||||
_vendors = const <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,
|
||||
},
|
||||
),
|
||||
];
|
||||
_selectedVendor = _vendors.first;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -707,7 +743,10 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
hours = endH - startH;
|
||||
if (hours < 0) hours += 24;
|
||||
} catch (_) {}
|
||||
total += hours * widget.order.hourlyRate * (pos['count'] as int);
|
||||
|
||||
final double rate =
|
||||
_selectedVendor?.rates[pos['role']] ?? widget.order.hourlyRate;
|
||||
total += hours * rate * (pos['count'] as int);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
@@ -741,6 +780,45 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
_buildSectionHeader('VENDOR'),
|
||||
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: _selectedVendor,
|
||||
icon: const Icon(
|
||||
UiIcons.chevronDown,
|
||||
size: 18,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onChanged: (Vendor? vendor) {
|
||||
if (vendor != null) {
|
||||
setState(() => _selectedVendor = vendor);
|
||||
}
|
||||
},
|
||||
items: _vendors.map((Vendor vendor) {
|
||||
return DropdownMenuItem<Vendor>(
|
||||
value: vendor,
|
||||
child: Text(
|
||||
vendor.name,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
_buildSectionHeader('DATE'),
|
||||
UiTextField(
|
||||
controller: _dateController,
|
||||
@@ -902,17 +980,8 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
hint: 'Select role',
|
||||
value: pos['role'],
|
||||
items: <String>[
|
||||
'Server',
|
||||
'Bartender',
|
||||
'Cook',
|
||||
'Busser',
|
||||
'Host',
|
||||
'Barista',
|
||||
'Dishwasher',
|
||||
'Event Staff',
|
||||
if (pos['role'] != null &&
|
||||
pos['role'].toString().isNotEmpty &&
|
||||
!<String>[
|
||||
...(_selectedVendor?.rates.keys.toList() ??
|
||||
<String>[
|
||||
'Server',
|
||||
'Bartender',
|
||||
'Cook',
|
||||
@@ -921,9 +990,17 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
'Barista',
|
||||
'Dishwasher',
|
||||
'Event Staff',
|
||||
].contains(pos['role']))
|
||||
]),
|
||||
if (pos['role'] != null &&
|
||||
pos['role'].toString().isNotEmpty &&
|
||||
!(_selectedVendor?.rates.keys.contains(pos['role']) ?? false))
|
||||
pos['role'].toString(),
|
||||
],
|
||||
itemBuilder: (dynamic role) {
|
||||
final double? rate = _selectedVendor?.rates[role];
|
||||
if (rate == null) return role.toString();
|
||||
return '$role - \$${rate.toStringAsFixed(0)}/hr';
|
||||
},
|
||||
onChanged: (dynamic val) => _updatePosition(index, 'role', val),
|
||||
),
|
||||
|
||||
@@ -1358,6 +1435,9 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
}
|
||||
|
||||
Widget _buildReviewPositionCard(Map<String, dynamic> pos) {
|
||||
final double rate =
|
||||
_selectedVendor?.rates[pos['role']] ?? widget.order.hourlyRate;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -1387,7 +1467,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'\$${widget.order.hourlyRate.round()}/hr',
|
||||
'\$${rate.round()}/hr',
|
||||
style: UiTypography.body2b.copyWith(color: UiColors.primary),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user