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.dart';
|
||||||
export 'src/entities/business/hub_department.dart';
|
export 'src/entities/business/hub_department.dart';
|
||||||
export 'src/entities/business/biz_contract.dart';
|
export 'src/entities/business/biz_contract.dart';
|
||||||
|
export 'src/entities/business/vendor.dart';
|
||||||
|
|
||||||
// Events & Shifts
|
// Events & Shifts
|
||||||
export 'src/entities/events/event.dart';
|
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> {
|
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||||
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
|
OneTimeOrderBloc(this._createOneTimeOrderUseCase)
|
||||||
: super(OneTimeOrderState.initial()) {
|
: super(OneTimeOrderState.initial()) {
|
||||||
|
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
|
||||||
|
on<OneTimeOrderVendorChanged>(_onVendorChanged);
|
||||||
on<OneTimeOrderDateChanged>(_onDateChanged);
|
on<OneTimeOrderDateChanged>(_onDateChanged);
|
||||||
on<OneTimeOrderLocationChanged>(_onLocationChanged);
|
on<OneTimeOrderLocationChanged>(_onLocationChanged);
|
||||||
on<OneTimeOrderPositionAdded>(_onPositionAdded);
|
on<OneTimeOrderPositionAdded>(_onPositionAdded);
|
||||||
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
|
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
|
||||||
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
|
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
|
||||||
on<OneTimeOrderSubmitted>(_onSubmitted);
|
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;
|
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(
|
void _onDateChanged(
|
||||||
OneTimeOrderDateChanged event,
|
OneTimeOrderDateChanged event,
|
||||||
Emitter<OneTimeOrderState> emit,
|
Emitter<OneTimeOrderState> emit,
|
||||||
@@ -41,8 +96,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
|||||||
const OneTimeOrderPosition(
|
const OneTimeOrderPosition(
|
||||||
role: '',
|
role: '',
|
||||||
count: 1,
|
count: 1,
|
||||||
startTime: '',
|
startTime: '09:00',
|
||||||
endTime: '',
|
endTime: '17:00',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
emit(state.copyWith(positions: newPositions));
|
emit(state.copyWith(positions: newPositions));
|
||||||
@@ -80,6 +135,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
|||||||
date: state.date,
|
date: state.date,
|
||||||
location: state.location,
|
location: state.location,
|
||||||
positions: state.positions,
|
positions: state.positions,
|
||||||
|
// In a real app, we'd pass the vendorId here
|
||||||
);
|
);
|
||||||
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
|
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
|
||||||
emit(state.copyWith(status: OneTimeOrderStatus.success));
|
emit(state.copyWith(status: OneTimeOrderStatus.success));
|
||||||
|
|||||||
@@ -8,6 +8,22 @@ abstract class OneTimeOrderEvent extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[];
|
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 {
|
class OneTimeOrderDateChanged extends OneTimeOrderEvent {
|
||||||
const OneTimeOrderDateChanged(this.date);
|
const OneTimeOrderDateChanged(this.date);
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class OneTimeOrderState extends Equatable {
|
|||||||
required this.positions,
|
required this.positions,
|
||||||
this.status = OneTimeOrderStatus.initial,
|
this.status = OneTimeOrderStatus.initial,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.vendors = const <Vendor>[],
|
||||||
|
this.selectedVendor,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory OneTimeOrderState.initial() {
|
factory OneTimeOrderState.initial() {
|
||||||
@@ -19,6 +21,7 @@ class OneTimeOrderState extends Equatable {
|
|||||||
positions: const <OneTimeOrderPosition>[
|
positions: const <OneTimeOrderPosition>[
|
||||||
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
|
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
|
||||||
],
|
],
|
||||||
|
vendors: const <Vendor>[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final DateTime date;
|
final DateTime date;
|
||||||
@@ -26,6 +29,8 @@ class OneTimeOrderState extends Equatable {
|
|||||||
final List<OneTimeOrderPosition> positions;
|
final List<OneTimeOrderPosition> positions;
|
||||||
final OneTimeOrderStatus status;
|
final OneTimeOrderStatus status;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
final List<Vendor> vendors;
|
||||||
|
final Vendor? selectedVendor;
|
||||||
|
|
||||||
OneTimeOrderState copyWith({
|
OneTimeOrderState copyWith({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
@@ -33,6 +38,8 @@ class OneTimeOrderState extends Equatable {
|
|||||||
List<OneTimeOrderPosition>? positions,
|
List<OneTimeOrderPosition>? positions,
|
||||||
OneTimeOrderStatus? status,
|
OneTimeOrderStatus? status,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
List<Vendor>? vendors,
|
||||||
|
Vendor? selectedVendor,
|
||||||
}) {
|
}) {
|
||||||
return OneTimeOrderState(
|
return OneTimeOrderState(
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
@@ -40,6 +47,8 @@ class OneTimeOrderState extends Equatable {
|
|||||||
positions: positions ?? this.positions,
|
positions: positions ?? this.positions,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
vendors: vendors ?? this.vendors,
|
||||||
|
selectedVendor: selectedVendor ?? this.selectedVendor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,5 +59,7 @@ class OneTimeOrderState extends Equatable {
|
|||||||
positions,
|
positions,
|
||||||
status,
|
status,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
vendors,
|
||||||
|
selectedVendor,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
required this.startLabel,
|
required this.startLabel,
|
||||||
required this.endLabel,
|
required this.endLabel,
|
||||||
required this.lunchLabel,
|
required this.lunchLabel,
|
||||||
|
this.vendor,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +56,9 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
/// Label for the lunch break.
|
/// Label for the lunch break.
|
||||||
final String lunchLabel;
|
final String lunchLabel;
|
||||||
|
|
||||||
|
/// The current selected vendor to determine rates.
|
||||||
|
final Vendor? vendor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -115,22 +119,21 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
items:
|
items:
|
||||||
<String>[
|
<String>{
|
||||||
'Server',
|
...(vendor?.rates.keys ?? <String>[]),
|
||||||
'Bartender',
|
if (position.role.isNotEmpty &&
|
||||||
'Cook',
|
!(vendor?.rates.keys.contains(position.role) ??
|
||||||
'Busser',
|
false))
|
||||||
'Host',
|
position.role,
|
||||||
'Barista',
|
}.map((String role) {
|
||||||
'Dishwasher',
|
final double? rate = vendor?.rates[role];
|
||||||
'Event Staff',
|
final String label = rate == null
|
||||||
].map((String role) {
|
? role
|
||||||
// Mock rates for UI matching
|
: '$role - \$${rate.toStringAsFixed(0)}/hr';
|
||||||
final int rate = _getMockRate(role);
|
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(
|
||||||
value: role,
|
value: role,
|
||||||
child: Text(
|
child: Text(
|
||||||
'$role - \$$rate/hr',
|
label,
|
||||||
style: UiTypography.body2r.textPrimary,
|
style: UiTypography.body2r.textPrimary,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -203,13 +206,13 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => onUpdated(
|
onTap: () {
|
||||||
position.copyWith(
|
if (position.count > 1) {
|
||||||
count: (position.count > 1)
|
onUpdated(
|
||||||
? position.count - 1
|
position.copyWith(count: position.count - 1),
|
||||||
: 1,
|
);
|
||||||
),
|
}
|
||||||
),
|
},
|
||||||
child: const Icon(UiIcons.minus, size: 12),
|
child: const Icon(UiIcons.minus, size: 12),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -217,9 +220,11 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
style: UiTypography.body2b.textPrimary,
|
style: UiTypography.body2b.textPrimary,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => onUpdated(
|
onTap: () {
|
||||||
position.copyWith(count: position.count + 1),
|
onUpdated(
|
||||||
),
|
position.copyWith(count: position.count + 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: const Icon(UiIcons.add, size: 12),
|
child: const Icon(UiIcons.add, size: 12),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -232,76 +237,12 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
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
|
// Lunch Break
|
||||||
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Container(
|
Container(
|
||||||
height: 44,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: UiConstants.radiusMd,
|
borderRadius: UiConstants.radiusMd,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
@@ -320,50 +261,15 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
onUpdated(position.copyWith(lunchBreak: val));
|
onUpdated(position.copyWith(lunchBreak: val));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
items: <DropdownMenuItem<int>>[
|
items: <int>[0, 15, 30, 45, 60].map((int mins) {
|
||||||
DropdownMenuItem<int>(
|
return DropdownMenuItem<int>(
|
||||||
value: 0,
|
value: mins,
|
||||||
child: Text(
|
child: Text(
|
||||||
t.client_create_order.one_time.no_break,
|
mins == 0 ? 'No Break' : '$mins mins',
|
||||||
style: UiTypography.body2r.textPrimary,
|
style: UiTypography.body2r.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
DropdownMenuItem<int>(
|
}).toList(),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -378,83 +284,37 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
required String value,
|
required String value,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
return UiTextField(
|
return Column(
|
||||||
label: label,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
controller: TextEditingController(text: value),
|
children: <Widget>[
|
||||||
readOnly: true,
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
onTap: onTap,
|
const SizedBox(height: UiConstants.space1),
|
||||||
hintText: '--:--',
|
GestureDetector(
|
||||||
);
|
onTap: onTap,
|
||||||
}
|
child: Container(
|
||||||
|
height: 40,
|
||||||
int _getMockRate(String role) {
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
switch (role) {
|
decoration: BoxDecoration(
|
||||||
case 'Server':
|
borderRadius: UiConstants.radiusSm,
|
||||||
return 18;
|
border: Border.all(color: UiColors.border),
|
||||||
case 'Bartender':
|
),
|
||||||
return 22;
|
child: Row(
|
||||||
case 'Cook':
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
return 20;
|
children: <Widget>[
|
||||||
case 'Busser':
|
Text(
|
||||||
return 16;
|
value.isEmpty ? '--:--' : value,
|
||||||
case 'Host':
|
style: UiTypography.body2r.textPrimary,
|
||||||
return 17;
|
),
|
||||||
case 'Barista':
|
const Icon(
|
||||||
return 16;
|
UiIcons.clock,
|
||||||
case 'Dishwasher':
|
size: 14,
|
||||||
return 15;
|
color: UiColors.iconSecondary,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
return Scaffold(
|
||||||
backgroundColor: UiColors.bgPrimary,
|
backgroundColor: UiColors.bgPrimary,
|
||||||
body: Column(
|
body: Column(
|
||||||
@@ -88,6 +129,47 @@ class _OneTimeOrderForm extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
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(
|
OneTimeOrderDatePicker(
|
||||||
label: labels.date_label,
|
label: labels.date_label,
|
||||||
value: state.date,
|
value: state.date,
|
||||||
@@ -133,6 +215,7 @@ class _OneTimeOrderForm extends StatelessWidget {
|
|||||||
startLabel: labels.start_label,
|
startLabel: labels.start_label,
|
||||||
endLabel: labels.end_label,
|
endLabel: labels.end_label,
|
||||||
lunchLabel: labels.lunch_break_label,
|
lunchLabel: labels.lunch_break_label,
|
||||||
|
vendor: state.selectedVendor,
|
||||||
onUpdated: (OneTimeOrderPosition updated) {
|
onUpdated: (OneTimeOrderPosition updated) {
|
||||||
BlocProvider.of<OneTimeOrderBloc>(
|
BlocProvider.of<OneTimeOrderBloc>(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -643,6 +643,9 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
|
|
||||||
late List<Map<String, dynamic>> _positions;
|
late List<Map<String, dynamic>> _positions;
|
||||||
|
|
||||||
|
List<Vendor> _vendors = const <Vendor>[];
|
||||||
|
Vendor? _selectedVendor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -661,6 +664,39 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
'location': null,
|
'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
|
@override
|
||||||
@@ -707,7 +743,10 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
hours = endH - startH;
|
hours = endH - startH;
|
||||||
if (hours < 0) hours += 24;
|
if (hours < 0) hours += 24;
|
||||||
} catch (_) {}
|
} 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;
|
return total;
|
||||||
}
|
}
|
||||||
@@ -741,6 +780,45 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
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'),
|
_buildSectionHeader('DATE'),
|
||||||
UiTextField(
|
UiTextField(
|
||||||
controller: _dateController,
|
controller: _dateController,
|
||||||
@@ -902,17 +980,8 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
hint: 'Select role',
|
hint: 'Select role',
|
||||||
value: pos['role'],
|
value: pos['role'],
|
||||||
items: <String>[
|
items: <String>[
|
||||||
'Server',
|
...(_selectedVendor?.rates.keys.toList() ??
|
||||||
'Bartender',
|
<String>[
|
||||||
'Cook',
|
|
||||||
'Busser',
|
|
||||||
'Host',
|
|
||||||
'Barista',
|
|
||||||
'Dishwasher',
|
|
||||||
'Event Staff',
|
|
||||||
if (pos['role'] != null &&
|
|
||||||
pos['role'].toString().isNotEmpty &&
|
|
||||||
!<String>[
|
|
||||||
'Server',
|
'Server',
|
||||||
'Bartender',
|
'Bartender',
|
||||||
'Cook',
|
'Cook',
|
||||||
@@ -921,9 +990,17 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
'Barista',
|
'Barista',
|
||||||
'Dishwasher',
|
'Dishwasher',
|
||||||
'Event Staff',
|
'Event Staff',
|
||||||
].contains(pos['role']))
|
]),
|
||||||
|
if (pos['role'] != null &&
|
||||||
|
pos['role'].toString().isNotEmpty &&
|
||||||
|
!(_selectedVendor?.rates.keys.contains(pos['role']) ?? false))
|
||||||
pos['role'].toString(),
|
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),
|
onChanged: (dynamic val) => _updatePosition(index, 'role', val),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -1358,6 +1435,9 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReviewPositionCard(Map<String, dynamic> pos) {
|
Widget _buildReviewPositionCard(Map<String, dynamic> pos) {
|
||||||
|
final double rate =
|
||||||
|
_selectedVendor?.rates[pos['role']] ?? widget.order.hourlyRate;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -1387,7 +1467,7 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'\$${widget.order.hourlyRate.round()}/hr',
|
'\$${rate.round()}/hr',
|
||||||
style: UiTypography.body2b.copyWith(color: UiColors.primary),
|
style: UiTypography.body2b.copyWith(color: UiColors.primary),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user