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:
Achintha Isuru
2026-01-23 12:05:04 -05:00
parent 868688fb02
commit d928dfb645
16 changed files with 840 additions and 250 deletions

View File

@@ -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 {
const _OrderEditSheet({required this.order});
@@ -726,37 +727,94 @@ class _OrderEditSheet extends StatefulWidget {
}
class _OrderEditSheetState extends State<_OrderEditSheet> {
late TextEditingController _titleController;
bool _showReview = false;
bool _isLoading = false;
late TextEditingController _dateController;
late TextEditingController _locationController;
late TextEditingController _workersNeededController;
late TextEditingController _globalLocationController;
// Local state for positions (starts with the single position from OrderItem)
late List<Map<String, dynamic>> _positions;
@override
void initState() {
super.initState();
_titleController = TextEditingController(text: widget.order.title);
_dateController = TextEditingController(text: widget.order.date);
_locationController = TextEditingController(
_globalLocationController = TextEditingController(
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
void dispose() {
_titleController.dispose();
_dateController.dispose();
_locationController.dispose();
_workersNeededController.dispose();
_globalLocationController.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
Widget build(BuildContext context) {
if (_isLoading && _showReview) {
return _buildSuccessView();
}
return _showReview ? _buildReviewView() : _buildFormView();
}
Widget _buildFormView() {
return Container(
height: MediaQuery.of(context).size.height * 0.9,
height: MediaQuery.of(context).size.height * 0.95,
decoration: const BoxDecoration(
color: UiColors.bgSecondary,
borderRadius: BorderRadius.vertical(
@@ -819,66 +877,289 @@ class _OrderEditSheetState extends State<_OrderEditSheet> {
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(UiConstants.space5),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildSectionLabel('Position Title'),
UiTextField(
controller: _titleController,
hintText: 'e.g. Server, Bartender',
prefixIcon: UiIcons.briefcase,
),
const SizedBox(height: UiConstants.space4),
_buildSectionLabel('Date'),
_buildSectionLabel('Date *'),
UiTextField(
controller: _dateController,
hintText: 'Select Date',
hintText: 'mm/dd/yyyy',
prefixIcon: UiIcons.calendar,
readOnly: true,
onTap: () {
// TODO: Show date picker
// TODO: Date picker
},
),
const SizedBox(height: UiConstants.space4),
const SizedBox(height: 16),
_buildSectionLabel('Location'),
_buildSectionLabel('Location *'),
UiTextField(
controller: _locationController,
controller: _globalLocationController,
hintText: 'Business address',
prefixIcon: UiIcons.mapPin,
),
const SizedBox(height: UiConstants.space4),
const SizedBox(height: 24),
_buildSectionLabel('Workers Needed'),
// Positions Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: UiTextField(
controller: _workersNeededController,
hintText: 'Quantity',
prefixIcon: UiIcons.users,
keyboardType: TextInputType.number,
Text(
'Positions',
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
),
UiButton.text(
leadingIcon: UiIcons.add,
text: 'Add Position',
onPressed: _addPosition,
),
],
),
const SizedBox(height: UiConstants.space6),
const SizedBox(height: 8),
UiButton.primary(
text: 'Save Changes',
fullWidth: true,
onPressed: () {
// TODO: Implement save logic
Navigator.pop(context);
},
..._positions.asMap().entries.map((
MapEntry<int, Map<String, dynamic>> entry,
) {
return _buildPositionCard(entry.key, entry.value);
}),
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(
text: 'Cancel',
fullWidth: true,
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
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) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),