Refactor order creation and edit UI for consistency
Refactored BLoC and widget code for order creation flows to improve code style, readability, and consistency. Unified the edit order bottom sheet to follow the Unified Order Flow prototype, supporting multiple positions, review, and confirmation steps. Updated UI components to use more concise widget tree structures and standardized button implementations.
This commit is contained in:
@@ -8,7 +8,7 @@ import 'client_create_order_state.dart';
|
|||||||
class ClientCreateOrderBloc
|
class ClientCreateOrderBloc
|
||||||
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
|
extends Bloc<ClientCreateOrderEvent, ClientCreateOrderState> {
|
||||||
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
||||||
: super(const ClientCreateOrderInitial()) {
|
: super(const ClientCreateOrderInitial()) {
|
||||||
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
||||||
}
|
}
|
||||||
final GetOrderTypesUseCase _getOrderTypesUseCase;
|
final GetOrderTypesUseCase _getOrderTypesUseCase;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'one_time_order_state.dart';
|
|||||||
/// BLoC for managing the multi-step one-time order creation form.
|
/// BLoC for managing the multi-step one-time order creation form.
|
||||||
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<OneTimeOrderDateChanged>(_onDateChanged);
|
on<OneTimeOrderDateChanged>(_onDateChanged);
|
||||||
on<OneTimeOrderLocationChanged>(_onLocationChanged);
|
on<OneTimeOrderLocationChanged>(_onLocationChanged);
|
||||||
on<OneTimeOrderPositionAdded>(_onPositionAdded);
|
on<OneTimeOrderPositionAdded>(_onPositionAdded);
|
||||||
@@ -37,13 +37,14 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
|||||||
Emitter<OneTimeOrderState> emit,
|
Emitter<OneTimeOrderState> emit,
|
||||||
) {
|
) {
|
||||||
final List<OneTimeOrderPosition> newPositions =
|
final List<OneTimeOrderPosition> newPositions =
|
||||||
List<OneTimeOrderPosition>.from(state.positions)
|
List<OneTimeOrderPosition>.from(state.positions)..add(
|
||||||
..add(const OneTimeOrderPosition(
|
const OneTimeOrderPosition(
|
||||||
role: '',
|
role: '',
|
||||||
count: 1,
|
count: 1,
|
||||||
startTime: '',
|
startTime: '',
|
||||||
endTime: '',
|
endTime: '',
|
||||||
));
|
),
|
||||||
|
);
|
||||||
emit(state.copyWith(positions: newPositions));
|
emit(state.copyWith(positions: newPositions));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +84,12 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState> {
|
|||||||
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
|
await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order));
|
||||||
emit(state.copyWith(status: OneTimeOrderStatus.success));
|
emit(state.copyWith(status: OneTimeOrderStatus.success));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(
|
emit(
|
||||||
status: OneTimeOrderStatus.failure,
|
state.copyWith(
|
||||||
errorMessage: e.toString(),
|
status: OneTimeOrderStatus.failure,
|
||||||
));
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,7 @@ class OneTimeOrderState extends Equatable {
|
|||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
location: '',
|
location: '',
|
||||||
positions: const <OneTimeOrderPosition>[
|
positions: const <OneTimeOrderPosition>[
|
||||||
OneTimeOrderPosition(
|
OneTimeOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
|
||||||
role: '',
|
|
||||||
count: 1,
|
|
||||||
startTime: '',
|
|
||||||
endTime: '',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,10 +45,10 @@ class OneTimeOrderState extends Equatable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
date,
|
date,
|
||||||
location,
|
location,
|
||||||
positions,
|
positions,
|
||||||
status,
|
status,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import 'rapid_order_state.dart';
|
|||||||
/// BLoC for managing the rapid (urgent) order creation flow.
|
/// BLoC for managing the rapid (urgent) order creation flow.
|
||||||
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
||||||
RapidOrderBloc(this._createRapidOrderUseCase)
|
RapidOrderBloc(this._createRapidOrderUseCase)
|
||||||
: super(
|
: super(
|
||||||
const RapidOrderInitial(
|
const RapidOrderInitial(
|
||||||
examples: <String>[
|
examples: <String>[
|
||||||
'"We had a call out. Need 2 cooks ASAP"',
|
'"We had a call out. Need 2 cooks ASAP"',
|
||||||
'"Need 5 bartenders ASAP until 5am"',
|
'"Need 5 bartenders ASAP until 5am"',
|
||||||
'"Emergency! Need 3 servers right now till midnight"',
|
'"Emergency! Need 3 servers right now till midnight"',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
on<RapidOrderMessageChanged>(_onMessageChanged);
|
on<RapidOrderMessageChanged>(_onMessageChanged);
|
||||||
on<RapidOrderVoiceToggled>(_onVoiceToggled);
|
on<RapidOrderVoiceToggled>(_onVoiceToggled);
|
||||||
on<RapidOrderSubmitted>(_onSubmitted);
|
on<RapidOrderSubmitted>(_onSubmitted);
|
||||||
@@ -68,7 +68,8 @@ class RapidOrderBloc extends Bloc<RapidOrderEvent, RapidOrderState> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await _createRapidOrderUseCase(
|
await _createRapidOrderUseCase(
|
||||||
RapidOrderArguments(description: message));
|
RapidOrderArguments(description: message),
|
||||||
|
);
|
||||||
emit(const RapidOrderSuccess());
|
emit(const RapidOrderSuccess());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(RapidOrderFailure(e.toString()));
|
emit(RapidOrderFailure(e.toString()));
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ class ClientCreateOrderPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ClientCreateOrderBloc>(
|
return BlocProvider<ClientCreateOrderBloc>(
|
||||||
create: (BuildContext context) => Modular.get<ClientCreateOrderBloc>()
|
create: (BuildContext context) =>
|
||||||
..add(const ClientCreateOrderTypesRequested()),
|
Modular.get<ClientCreateOrderBloc>()
|
||||||
|
..add(const ClientCreateOrderTypesRequested()),
|
||||||
child: const CreateOrderView(),
|
child: const CreateOrderView(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,59 +65,58 @@ class CreateOrderView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child: BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
|
||||||
BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
|
|
||||||
builder:
|
builder:
|
||||||
(BuildContext context, ClientCreateOrderState state) {
|
(BuildContext context, ClientCreateOrderState state) {
|
||||||
if (state is ClientCreateOrderLoadSuccess) {
|
if (state is ClientCreateOrderLoadSuccess) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
gridDelegate:
|
gridDelegate:
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
mainAxisSpacing: UiConstants.space4,
|
mainAxisSpacing: UiConstants.space4,
|
||||||
crossAxisSpacing: UiConstants.space4,
|
crossAxisSpacing: UiConstants.space4,
|
||||||
childAspectRatio: 1,
|
childAspectRatio: 1,
|
||||||
),
|
),
|
||||||
itemCount: state.orderTypes.length,
|
itemCount: state.orderTypes.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final OrderType type = state.orderTypes[index];
|
final OrderType type = state.orderTypes[index];
|
||||||
final OrderTypeUiMetadata ui =
|
final OrderTypeUiMetadata ui =
|
||||||
OrderTypeUiMetadata.fromId(id: type.id);
|
OrderTypeUiMetadata.fromId(id: type.id);
|
||||||
|
|
||||||
return OrderTypeCard(
|
return OrderTypeCard(
|
||||||
icon: ui.icon,
|
icon: ui.icon,
|
||||||
title: _getTranslation(key: type.titleKey),
|
title: _getTranslation(key: type.titleKey),
|
||||||
description: _getTranslation(
|
description: _getTranslation(
|
||||||
key: type.descriptionKey,
|
key: type.descriptionKey,
|
||||||
),
|
),
|
||||||
backgroundColor: ui.backgroundColor,
|
backgroundColor: ui.backgroundColor,
|
||||||
borderColor: ui.borderColor,
|
borderColor: ui.borderColor,
|
||||||
iconBackgroundColor: ui.iconBackgroundColor,
|
iconBackgroundColor: ui.iconBackgroundColor,
|
||||||
iconColor: ui.iconColor,
|
iconColor: ui.iconColor,
|
||||||
textColor: ui.textColor,
|
textColor: ui.textColor,
|
||||||
descriptionColor: ui.descriptionColor,
|
descriptionColor: ui.descriptionColor,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
switch (type.id) {
|
switch (type.id) {
|
||||||
case 'rapid':
|
case 'rapid':
|
||||||
Modular.to.pushRapidOrder();
|
Modular.to.pushRapidOrder();
|
||||||
break;
|
break;
|
||||||
case 'one-time':
|
case 'one-time':
|
||||||
Modular.to.pushOneTimeOrder();
|
Modular.to.pushOneTimeOrder();
|
||||||
break;
|
break;
|
||||||
case 'recurring':
|
case 'recurring':
|
||||||
Modular.to.pushRecurringOrder();
|
Modular.to.pushRecurringOrder();
|
||||||
break;
|
break;
|
||||||
case 'permanent':
|
case 'permanent':
|
||||||
Modular.to.pushPermanentOrder();
|
Modular.to.pushPermanentOrder();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
},
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ class OneTimeOrderHeader extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: UiTypography.headline3m.copyWith(
|
style: UiTypography.headline3m.copyWith(color: UiColors.white),
|
||||||
color: UiColors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
|
|||||||
@@ -99,8 +99,10 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<String>(
|
child: DropdownButton<String>(
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
hint:
|
hint: Text(
|
||||||
Text(roleLabel, style: UiTypography.body2r.textPlaceholder),
|
roleLabel,
|
||||||
|
style: UiTypography.body2r.textPlaceholder,
|
||||||
|
),
|
||||||
value: position.role.isEmpty ? null : position.role,
|
value: position.role.isEmpty ? null : position.role,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
UiIcons.chevronDown,
|
UiIcons.chevronDown,
|
||||||
@@ -112,26 +114,27 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
onUpdated(position.copyWith(role: val));
|
onUpdated(position.copyWith(role: val));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
items: <String>[
|
items:
|
||||||
'Server',
|
<String>[
|
||||||
'Bartender',
|
'Server',
|
||||||
'Cook',
|
'Bartender',
|
||||||
'Busser',
|
'Cook',
|
||||||
'Host',
|
'Busser',
|
||||||
'Barista',
|
'Host',
|
||||||
'Dishwasher',
|
'Barista',
|
||||||
'Event Staff'
|
'Dishwasher',
|
||||||
].map((String role) {
|
'Event Staff',
|
||||||
// Mock rates for UI matching
|
].map((String role) {
|
||||||
final int rate = _getMockRate(role);
|
// Mock rates for UI matching
|
||||||
return DropdownMenuItem<String>(
|
final int rate = _getMockRate(role);
|
||||||
value: role,
|
return DropdownMenuItem<String>(
|
||||||
child: Text(
|
value: role,
|
||||||
'$role - \$$rate/hr',
|
child: Text(
|
||||||
style: UiTypography.body2r.textPrimary,
|
'$role - \$$rate/hr',
|
||||||
),
|
style: UiTypography.body2r.textPrimary,
|
||||||
);
|
),
|
||||||
}).toList(),
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -153,7 +156,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (picked != null && context.mounted) {
|
if (picked != null && context.mounted) {
|
||||||
onUpdated(
|
onUpdated(
|
||||||
position.copyWith(startTime: picked.format(context)));
|
position.copyWith(startTime: picked.format(context)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -172,7 +176,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
if (picked != null && context.mounted) {
|
if (picked != null && context.mounted) {
|
||||||
onUpdated(
|
onUpdated(
|
||||||
position.copyWith(endTime: picked.format(context)));
|
position.copyWith(endTime: picked.format(context)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -198,10 +203,13 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => onUpdated(position.copyWith(
|
onTap: () => onUpdated(
|
||||||
|
position.copyWith(
|
||||||
count: (position.count > 1)
|
count: (position.count > 1)
|
||||||
? position.count - 1
|
? position.count - 1
|
||||||
: 1)),
|
: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: const Icon(UiIcons.minus, size: 12),
|
child: const Icon(UiIcons.minus, size: 12),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -210,7 +218,8 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => onUpdated(
|
onTap: () => onUpdated(
|
||||||
position.copyWith(count: position.count + 1)),
|
position.copyWith(count: position.count + 1),
|
||||||
|
),
|
||||||
child: const Icon(UiIcons.add, size: 12),
|
child: const Icon(UiIcons.add, size: 12),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -249,11 +258,16 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(UiIcons.mapPin,
|
const Icon(
|
||||||
size: 14, color: UiColors.iconSecondary),
|
UiIcons.mapPin,
|
||||||
|
size: 14,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
const SizedBox(width: UiConstants.space1),
|
const SizedBox(width: UiConstants.space1),
|
||||||
Text(
|
Text(
|
||||||
t.client_create_order.one_time
|
t
|
||||||
|
.client_create_order
|
||||||
|
.one_time
|
||||||
.different_location_title,
|
.different_location_title,
|
||||||
style: UiTypography.footnote1m.textSecondary,
|
style: UiTypography.footnote1m.textSecondary,
|
||||||
),
|
),
|
||||||
@@ -283,10 +297,7 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
// Lunch Break
|
// Lunch Break
|
||||||
Text(
|
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
||||||
lunchLabel,
|
|
||||||
style: UiTypography.footnote2r.textSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Container(
|
Container(
|
||||||
height: 44,
|
height: 44,
|
||||||
@@ -312,38 +323,45 @@ class OneTimeOrderPositionCard extends StatelessWidget {
|
|||||||
items: <DropdownMenuItem<int>>[
|
items: <DropdownMenuItem<int>>[
|
||||||
DropdownMenuItem<int>(
|
DropdownMenuItem<int>(
|
||||||
value: 0,
|
value: 0,
|
||||||
child: Text(t.client_create_order.one_time.no_break,
|
child: Text(
|
||||||
style: UiTypography.body2r.textPrimary),
|
t.client_create_order.one_time.no_break,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DropdownMenuItem<int>(
|
DropdownMenuItem<int>(
|
||||||
value: 10,
|
value: 10,
|
||||||
child: Text(
|
child: Text(
|
||||||
'10 ${t.client_create_order.one_time.paid_break}',
|
'10 ${t.client_create_order.one_time.paid_break}',
|
||||||
style: UiTypography.body2r.textPrimary),
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DropdownMenuItem<int>(
|
DropdownMenuItem<int>(
|
||||||
value: 15,
|
value: 15,
|
||||||
child: Text(
|
child: Text(
|
||||||
'15 ${t.client_create_order.one_time.paid_break}',
|
'15 ${t.client_create_order.one_time.paid_break}',
|
||||||
style: UiTypography.body2r.textPrimary),
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DropdownMenuItem<int>(
|
DropdownMenuItem<int>(
|
||||||
value: 30,
|
value: 30,
|
||||||
child: Text(
|
child: Text(
|
||||||
'30 ${t.client_create_order.one_time.unpaid_break}',
|
'30 ${t.client_create_order.one_time.unpaid_break}',
|
||||||
style: UiTypography.body2r.textPrimary),
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DropdownMenuItem<int>(
|
DropdownMenuItem<int>(
|
||||||
value: 45,
|
value: 45,
|
||||||
child: Text(
|
child: Text(
|
||||||
'45 ${t.client_create_order.one_time.unpaid_break}',
|
'45 ${t.client_create_order.one_time.unpaid_break}',
|
||||||
style: UiTypography.body2r.textPrimary),
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
DropdownMenuItem<int>(
|
DropdownMenuItem<int>(
|
||||||
value: 60,
|
value: 60,
|
||||||
child: Text(
|
child: Text(
|
||||||
'60 ${t.client_create_order.one_time.unpaid_break}',
|
'60 ${t.client_create_order.one_time.unpaid_break}',
|
||||||
style: UiTypography.body2r.textPrimary),
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -27,14 +27,28 @@ class OneTimeOrderSectionHeader extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(title, style: UiTypography.headline4m.textPrimary),
|
Text(title, style: UiTypography.headline4m.textPrimary),
|
||||||
if (actionLabel != null && onAction != null)
|
if (actionLabel != null && onAction != null)
|
||||||
UiButton.text(
|
TextButton(
|
||||||
onPressed: onAction,
|
onPressed: onAction,
|
||||||
leadingIcon: UiIcons.add,
|
|
||||||
text: actionLabel!,
|
|
||||||
iconSize: 16,
|
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
minimumSize: const Size(0, 24),
|
padding: EdgeInsets.zero,
|
||||||
maximumSize: const Size(0, 24),
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ class OneTimeOrderView extends StatelessWidget {
|
|||||||
? labels.creating
|
? labels.creating
|
||||||
: labels.create_order,
|
: labels.create_order,
|
||||||
isLoading: state.status == OneTimeOrderStatus.loading,
|
isLoading: state.status == OneTimeOrderStatus.loading,
|
||||||
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(context)
|
onPressed: () => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
.add(const OneTimeOrderSubmitted()),
|
context,
|
||||||
|
).add(const OneTimeOrderSubmitted()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -90,34 +91,34 @@ class _OneTimeOrderForm extends StatelessWidget {
|
|||||||
OneTimeOrderDatePicker(
|
OneTimeOrderDatePicker(
|
||||||
label: labels.date_label,
|
label: labels.date_label,
|
||||||
value: state.date,
|
value: state.date,
|
||||||
onChanged: (DateTime date) =>
|
onChanged: (DateTime date) => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context)
|
context,
|
||||||
.add(OneTimeOrderDateChanged(date)),
|
).add(OneTimeOrderDateChanged(date)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
OneTimeOrderLocationInput(
|
OneTimeOrderLocationInput(
|
||||||
label: labels.location_label,
|
label: labels.location_label,
|
||||||
value: state.location,
|
value: state.location,
|
||||||
onChanged: (String location) =>
|
onChanged: (String location) => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context)
|
context,
|
||||||
.add(OneTimeOrderLocationChanged(location)),
|
).add(OneTimeOrderLocationChanged(location)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
|
||||||
OneTimeOrderSectionHeader(
|
OneTimeOrderSectionHeader(
|
||||||
title: labels.positions_title,
|
title: labels.positions_title,
|
||||||
actionLabel: labels.add_position,
|
actionLabel: labels.add_position,
|
||||||
onAction: () => BlocProvider.of<OneTimeOrderBloc>(context)
|
onAction: () => BlocProvider.of<OneTimeOrderBloc>(
|
||||||
.add(const OneTimeOrderPositionAdded()),
|
context,
|
||||||
|
).add(const OneTimeOrderPositionAdded()),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
// Positions List
|
// Positions List
|
||||||
...state.positions
|
...state.positions.asMap().entries.map((
|
||||||
.asMap()
|
MapEntry<int, OneTimeOrderPosition> entry,
|
||||||
.entries
|
) {
|
||||||
.map((MapEntry<int, OneTimeOrderPosition> entry) {
|
|
||||||
final int index = entry.key;
|
final int index = entry.key;
|
||||||
final OneTimeOrderPosition position = entry.value;
|
final OneTimeOrderPosition position = entry.value;
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -133,13 +134,14 @@ class _OneTimeOrderForm extends StatelessWidget {
|
|||||||
endLabel: labels.end_label,
|
endLabel: labels.end_label,
|
||||||
lunchLabel: labels.lunch_break_label,
|
lunchLabel: labels.lunch_break_label,
|
||||||
onUpdated: (OneTimeOrderPosition updated) {
|
onUpdated: (OneTimeOrderPosition updated) {
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context).add(
|
BlocProvider.of<OneTimeOrderBloc>(
|
||||||
OneTimeOrderPositionUpdated(index, updated),
|
context,
|
||||||
);
|
).add(OneTimeOrderPositionUpdated(index, updated));
|
||||||
},
|
},
|
||||||
onRemoved: () {
|
onRemoved: () {
|
||||||
BlocProvider.of<OneTimeOrderBloc>(context)
|
BlocProvider.of<OneTimeOrderBloc>(
|
||||||
.add(OneTimeOrderPositionRemoved(index));
|
context,
|
||||||
|
).add(OneTimeOrderPositionRemoved(index));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,16 +73,14 @@ class OrderTypeCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Icon(icon, color: iconColor, size: 24),
|
child: Icon(icon, color: iconColor, size: 24),
|
||||||
),
|
),
|
||||||
Text(
|
Text(title, style: UiTypography.body2b.copyWith(color: textColor)),
|
||||||
title,
|
|
||||||
style: UiTypography.body2b.copyWith(color: textColor),
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
description,
|
description,
|
||||||
style:
|
style: UiTypography.footnote1r.copyWith(
|
||||||
UiTypography.footnote1r.copyWith(color: descriptionColor),
|
color: descriptionColor,
|
||||||
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ class RapidOrderExampleCard extends StatelessWidget {
|
|||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: UiTypography.body2r.textPrimary,
|
style: UiTypography.body2r.textPrimary,
|
||||||
children: <InlineSpan>[
|
children: <InlineSpan>[
|
||||||
TextSpan(
|
TextSpan(text: label, style: UiTypography.body2b.textPrimary),
|
||||||
text: label,
|
|
||||||
style: UiTypography.body2b.textPrimary,
|
|
||||||
),
|
|
||||||
TextSpan(text: ' $example'),
|
TextSpan(text: ' $example'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -74,11 +74,7 @@ class RapidOrderHeader extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Icon(
|
const Icon(UiIcons.zap, color: UiColors.accent, size: 18),
|
||||||
UiIcons.zap,
|
|
||||||
color: UiColors.accent,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ class RapidOrderSuccessView extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin:
|
margin: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: UiConstants.space10),
|
horizontal: UiConstants.space10,
|
||||||
|
),
|
||||||
padding: const EdgeInsets.all(UiConstants.space8),
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
@@ -75,10 +76,7 @@ class RapidOrderSuccessView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: UiConstants.space6),
|
||||||
Text(
|
Text(title, style: UiTypography.headline1m.textPrimary),
|
||||||
title,
|
|
||||||
style: UiTypography.headline1m.textPrimary,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
Text(
|
Text(
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -153,27 +153,27 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
|
|
||||||
// Examples
|
// Examples
|
||||||
if (initialState != null)
|
if (initialState != null)
|
||||||
...initialState.examples
|
...initialState.examples.asMap().entries.map((
|
||||||
.asMap()
|
MapEntry<int, String> entry,
|
||||||
.entries
|
) {
|
||||||
.map((MapEntry<int, String> entry) {
|
|
||||||
final int index = entry.key;
|
final int index = entry.key;
|
||||||
final String example = entry.value;
|
final String example = entry.value;
|
||||||
final bool isHighlighted = index == 0;
|
final bool isHighlighted = index == 0;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
bottom: UiConstants.space2),
|
bottom: UiConstants.space2,
|
||||||
|
),
|
||||||
child: RapidOrderExampleCard(
|
child: RapidOrderExampleCard(
|
||||||
example: example,
|
example: example,
|
||||||
isHighlighted: isHighlighted,
|
isHighlighted: isHighlighted,
|
||||||
label: labels.example,
|
label: labels.example,
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
BlocProvider.of<RapidOrderBloc>(
|
BlocProvider.of<RapidOrderBloc>(
|
||||||
context)
|
context,
|
||||||
.add(
|
).add(
|
||||||
RapidOrderExampleSelected(example),
|
RapidOrderExampleSelected(example),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -184,9 +184,9 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
onChanged: (String value) {
|
onChanged: (String value) {
|
||||||
BlocProvider.of<RapidOrderBloc>(context).add(
|
BlocProvider.of<RapidOrderBloc>(
|
||||||
RapidOrderMessageChanged(value),
|
context,
|
||||||
);
|
).add(RapidOrderMessageChanged(value));
|
||||||
},
|
},
|
||||||
hintText: labels.hint,
|
hintText: labels.hint,
|
||||||
),
|
),
|
||||||
@@ -197,7 +197,8 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
|
|||||||
labels: labels,
|
labels: labels,
|
||||||
isSubmitting: isSubmitting,
|
isSubmitting: isSubmitting,
|
||||||
isListening: initialState?.isListening ?? false,
|
isListening: initialState?.isListening ?? false,
|
||||||
isMessageEmpty: initialState != null &&
|
isMessageEmpty:
|
||||||
|
initialState != null &&
|
||||||
initialState.message.trim().isEmpty,
|
initialState.message.trim().isEmpty,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -242,11 +243,7 @@ class _AnimatedZapIcon extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(UiIcons.zap, color: UiColors.white, size: 32),
|
||||||
UiIcons.zap,
|
|
||||||
color: UiColors.white,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,9 +268,9 @@ class _RapidOrderActions extends StatelessWidget {
|
|||||||
child: UiButton.secondary(
|
child: UiButton.secondary(
|
||||||
text: isListening ? labels.listening : labels.speak,
|
text: isListening ? labels.listening : labels.speak,
|
||||||
leadingIcon: UiIcons.bell, // Placeholder for mic
|
leadingIcon: UiIcons.bell, // Placeholder for mic
|
||||||
onPressed: () => BlocProvider.of<RapidOrderBloc>(context).add(
|
onPressed: () => BlocProvider.of<RapidOrderBloc>(
|
||||||
const RapidOrderVoiceToggled(),
|
context,
|
||||||
),
|
).add(const RapidOrderVoiceToggled()),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
backgroundColor: isListening
|
backgroundColor: isListening
|
||||||
? UiColors.destructive.withValues(alpha: 0.05)
|
? UiColors.destructive.withValues(alpha: 0.05)
|
||||||
@@ -291,9 +288,9 @@ class _RapidOrderActions extends StatelessWidget {
|
|||||||
trailingIcon: UiIcons.arrowRight,
|
trailingIcon: UiIcons.arrowRight,
|
||||||
onPressed: isSubmitting || isMessageEmpty
|
onPressed: isSubmitting || isMessageEmpty
|
||||||
? null
|
? null
|
||||||
: () => BlocProvider.of<RapidOrderBloc>(context).add(
|
: () => BlocProvider.of<RapidOrderBloc>(
|
||||||
const RapidOrderSubmitted(),
|
context,
|
||||||
),
|
).add(const RapidOrderSubmitted()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -715,7 +715,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bottom sheet for editing an existing order.
|
/// A sophisticated bottom sheet for editing an existing order,
|
||||||
|
/// following the Unified Order Flow prototype.
|
||||||
class _OrderEditSheet extends StatefulWidget {
|
class _OrderEditSheet extends StatefulWidget {
|
||||||
const _OrderEditSheet({required this.order});
|
const _OrderEditSheet({required this.order});
|
||||||
|
|
||||||
@@ -726,37 +727,94 @@ class _OrderEditSheet extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _OrderEditSheetState extends State<_OrderEditSheet> {
|
class _OrderEditSheetState extends State<_OrderEditSheet> {
|
||||||
late TextEditingController _titleController;
|
bool _showReview = false;
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
late TextEditingController _dateController;
|
late TextEditingController _dateController;
|
||||||
late TextEditingController _locationController;
|
late TextEditingController _globalLocationController;
|
||||||
late TextEditingController _workersNeededController;
|
|
||||||
|
// Local state for positions (starts with the single position from OrderItem)
|
||||||
|
late List<Map<String, dynamic>> _positions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_titleController = TextEditingController(text: widget.order.title);
|
|
||||||
_dateController = TextEditingController(text: widget.order.date);
|
_dateController = TextEditingController(text: widget.order.date);
|
||||||
_locationController = TextEditingController(
|
_globalLocationController = TextEditingController(
|
||||||
text: widget.order.locationAddress,
|
text: widget.order.locationAddress,
|
||||||
);
|
);
|
||||||
_workersNeededController = TextEditingController(
|
|
||||||
text: widget.order.workersNeeded.toString(),
|
_positions = <Map<String, dynamic>>[
|
||||||
);
|
<String, dynamic>{
|
||||||
|
'role': widget.order.title,
|
||||||
|
'count': widget.order.workersNeeded,
|
||||||
|
'start_time': widget.order.startTime,
|
||||||
|
'end_time': widget.order.endTime,
|
||||||
|
'location': '', // Specific location if different from global
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_titleController.dispose();
|
|
||||||
_dateController.dispose();
|
_dateController.dispose();
|
||||||
_locationController.dispose();
|
_globalLocationController.dispose();
|
||||||
_workersNeededController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _addPosition() {
|
||||||
|
setState(() {
|
||||||
|
_positions.add(<String, dynamic>{
|
||||||
|
'role': '',
|
||||||
|
'count': 1,
|
||||||
|
'start_time': '09:00',
|
||||||
|
'end_time': '17:00',
|
||||||
|
'location': '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removePosition(int index) {
|
||||||
|
if (_positions.length > 1) {
|
||||||
|
setState(() => _positions.removeAt(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updatePosition(int index, String key, dynamic value) {
|
||||||
|
setState(() => _positions[index][key] = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculateTotalCost() {
|
||||||
|
double total = 0;
|
||||||
|
for (final Map<String, dynamic> pos in _positions) {
|
||||||
|
double hours = 8; // Default fallback
|
||||||
|
try {
|
||||||
|
final List<String> startParts = pos['start_time'].toString().split(':');
|
||||||
|
final List<String> endParts = pos['end_time'].toString().split(':');
|
||||||
|
final double startH =
|
||||||
|
int.parse(startParts[0]) + int.parse(startParts[1]) / 60;
|
||||||
|
final double endH =
|
||||||
|
int.parse(endParts[0]) + int.parse(endParts[1]) / 60;
|
||||||
|
hours = endH - startH;
|
||||||
|
if (hours < 0) hours += 24;
|
||||||
|
} catch (_) {}
|
||||||
|
total += hours * widget.order.hourlyRate * (pos['count'] as int);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_isLoading && _showReview) {
|
||||||
|
return _buildSuccessView();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _showReview ? _buildReviewView() : _buildFormView();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormView() {
|
||||||
return Container(
|
return Container(
|
||||||
height: MediaQuery.of(context).size.height * 0.9,
|
height: MediaQuery.of(context).size.height * 0.95,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: UiColors.bgSecondary,
|
color: UiColors.bgSecondary,
|
||||||
borderRadius: BorderRadius.vertical(
|
borderRadius: BorderRadius.vertical(
|
||||||
@@ -819,66 +877,289 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
// Content
|
// Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_buildSectionLabel('Position Title'),
|
_buildSectionLabel('Date *'),
|
||||||
UiTextField(
|
|
||||||
controller: _titleController,
|
|
||||||
hintText: 'e.g. Server, Bartender',
|
|
||||||
prefixIcon: UiIcons.briefcase,
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
|
||||||
|
|
||||||
_buildSectionLabel('Date'),
|
|
||||||
UiTextField(
|
UiTextField(
|
||||||
controller: _dateController,
|
controller: _dateController,
|
||||||
hintText: 'Select Date',
|
hintText: 'mm/dd/yyyy',
|
||||||
prefixIcon: UiIcons.calendar,
|
prefixIcon: UiIcons.calendar,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// TODO: Show date picker
|
// TODO: Date picker
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
_buildSectionLabel('Location'),
|
_buildSectionLabel('Location *'),
|
||||||
UiTextField(
|
UiTextField(
|
||||||
controller: _locationController,
|
controller: _globalLocationController,
|
||||||
hintText: 'Business address',
|
hintText: 'Business address',
|
||||||
prefixIcon: UiIcons.mapPin,
|
prefixIcon: UiIcons.mapPin,
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
_buildSectionLabel('Workers Needed'),
|
// Positions Header
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Text(
|
||||||
child: UiTextField(
|
'Positions',
|
||||||
controller: _workersNeededController,
|
style: UiTypography.title1m.copyWith(
|
||||||
hintText: 'Quantity',
|
color: UiColors.textPrimary,
|
||||||
prefixIcon: UiIcons.users,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
UiButton.text(
|
||||||
|
leadingIcon: UiIcons.add,
|
||||||
|
text: 'Add Position',
|
||||||
|
onPressed: _addPosition,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space6),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
UiButton.primary(
|
..._positions.asMap().entries.map((
|
||||||
text: 'Save Changes',
|
MapEntry<int, Map<String, dynamic>> entry,
|
||||||
fullWidth: true,
|
) {
|
||||||
onPressed: () {
|
return _buildPositionCard(entry.key, entry.value);
|
||||||
// TODO: Implement save logic
|
}),
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
border: Border(top: BorderSide(color: UiColors.separatorPrimary)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: 'Review ${_positions.length} Positions',
|
||||||
|
fullWidth: true,
|
||||||
|
onPressed: () => setState(() => _showReview = true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReviewView() {
|
||||||
|
final int totalWorkers = _positions.fold<int>(
|
||||||
|
0,
|
||||||
|
(int sum, Map<String, dynamic> p) => sum + (p['count'] as int),
|
||||||
|
);
|
||||||
|
final double totalCost = _calculateTotalCost();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.95,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.bgSecondary,
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(UiConstants.radiusBase * 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(UiConstants.radiusBase * 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => setState(() => _showReview = false),
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.chevronLeft,
|
||||||
|
color: UiColors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space3),
|
),
|
||||||
UiButton.ghost(
|
const SizedBox(width: 12),
|
||||||
text: 'Cancel',
|
Column(
|
||||||
fullWidth: true,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
onPressed: () => Navigator.pop(context),
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'Review Order',
|
||||||
|
style: UiTypography.title1m.copyWith(
|
||||||
|
color: UiColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Confirm details before saving',
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Summary Card
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: <Color>[
|
||||||
|
UiColors.primary.withValues(alpha: 0.05),
|
||||||
|
UiColors.primary.withValues(alpha: 0.1),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.primary.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildSummaryItem('${_positions.length}', 'Positions'),
|
||||||
|
_buildSummaryItem('$totalWorkers', 'Workers'),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'\$${totalCost.round()}',
|
||||||
|
'Est. Cost',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Order Details
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: UiColors.separatorPrimary),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.calendar,
|
||||||
|
size: 16,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_dateController.text,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_globalLocationController
|
||||||
|
.text
|
||||||
|
.isNotEmpty) ...<Widget>[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.mapPin,
|
||||||
|
size: 16,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_globalLocationController.text,
|
||||||
|
style: UiTypography.body2r.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Positions Breakdown',
|
||||||
|
style: UiTypography.body2b.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
..._positions.map(
|
||||||
|
(Map<String, dynamic> pos) => _buildReviewPositionCard(pos),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
border: Border(top: BorderSide(color: UiColors.separatorPrimary)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: UiButton.secondary(
|
||||||
|
text: 'Edit',
|
||||||
|
onPressed: () => setState(() => _showReview = false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: 'Confirm & Save',
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
if (mounted) {
|
||||||
|
// TODO: Implement actual save logic
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -889,6 +1170,298 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryItem(String value, String label) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: UiTypography.headline2m.copyWith(
|
||||||
|
color: UiColors.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label.toUpperCase(),
|
||||||
|
style: UiTypography.titleUppercase4m.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPositionCard(int index, Map<String, dynamic> pos) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: UiColors.separatorSecondary, width: 2),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'${index + 1}',
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: UiColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Position ${index + 1}',
|
||||||
|
style: UiTypography.footnote2m.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_positions.length > 1)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _removePosition(index),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFFEF2F2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
UiIcons.close,
|
||||||
|
size: 14,
|
||||||
|
color: UiColors.destructive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
_buildSectionLabel('Position Title *'),
|
||||||
|
UiTextField(
|
||||||
|
controller: TextEditingController(text: pos['role']),
|
||||||
|
hintText: 'e.g. Server, Bartender',
|
||||||
|
prefixIcon: UiIcons.briefcase,
|
||||||
|
onChanged: (String val) => _updatePosition(index, 'role', val),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildSectionLabel('Start Time *'),
|
||||||
|
UiTextField(
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: pos['start_time'],
|
||||||
|
),
|
||||||
|
prefixIcon: UiIcons.clock,
|
||||||
|
onTap: () {}, // Time picker
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildSectionLabel('End Time *'),
|
||||||
|
UiTextField(
|
||||||
|
controller: TextEditingController(text: pos['end_time']),
|
||||||
|
prefixIcon: UiIcons.clock,
|
||||||
|
onTap: () {}, // Time picker
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
_buildSectionLabel('Workers Needed'),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
_buildCounterBtn(
|
||||||
|
icon: UiIcons.minus,
|
||||||
|
onTap: () {
|
||||||
|
if ((pos['count'] as int) > 1) {
|
||||||
|
_updatePosition(index, 'count', (pos['count'] as int) - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text('${pos['count']}', style: UiTypography.body1b),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
_buildCounterBtn(
|
||||||
|
icon: UiIcons.add,
|
||||||
|
onTap: () {
|
||||||
|
_updatePosition(index, 'count', (pos['count'] as int) + 1);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildReviewPositionCard(Map<String, dynamic> pos) {
|
||||||
|
// Simplified cost calculation
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: UiColors.separatorSecondary),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
pos['role'].toString().isEmpty
|
||||||
|
? 'Position'
|
||||||
|
: pos['role'].toString(),
|
||||||
|
style: UiTypography.body2b.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${pos['count']} worker${pos['count'] > 1 ? 's' : ''}',
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'\$${widget.order.hourlyRate.round()}/hr',
|
||||||
|
style: UiTypography.body2b.copyWith(color: UiColors.primary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 14,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'${pos['start_time']} - ${pos['end_time']}',
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCounterBtn({
|
||||||
|
required IconData icon,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgSecondary,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 16, color: UiColors.primary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSuccessView() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: MediaQuery.of(context).size.height * 0.95,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.accent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.success,
|
||||||
|
size: 40,
|
||||||
|
color: UiColors.foreground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Order Updated!',
|
||||||
|
style: UiTypography.headline1m.copyWith(color: UiColors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: Text(
|
||||||
|
'Your shift has been updated successfully.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: UiTypography.body1r.copyWith(
|
||||||
|
color: UiColors.white.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
child: UiButton.secondary(
|
||||||
|
text: 'Back to Orders',
|
||||||
|
fullWidth: true,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.white,
|
||||||
|
foregroundColor: UiColors.primary,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSectionLabel(String label) {
|
Widget _buildSectionLabel(String label) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
|||||||
Reference in New Issue
Block a user