Files
Xpress-rider/lib/views/Dashboard/orders/orderstaus_button.dart

612 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'dart:async';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:slide_to_submit_button/slide_to_submit_button.dart';
import 'package:image_picker/image_picker.dart';
/// ------------------------------
/// MAIN WIDGET WITH TWO BUTTONS
/// ------------------------------
class OrderStatusRow extends StatefulWidget {
final String currentStatus;
final Future<bool> Function(String newStatus, {String? notes, String? proofImagePath}) onStatusChange;
final bool enabled;
const OrderStatusRow({
super.key,
required this.currentStatus,
required this.onStatusChange,
this.enabled = true,
});
@override
State<OrderStatusRow> createState() => _OrderStatusRowState();
}
class _OrderStatusRowState extends State<OrderStatusRow> {
bool _isProcessing = false;
String? _overrideStatus;
@override
void didUpdateWidget(covariant OrderStatusRow oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentStatus != widget.currentStatus &&
mounted &&
_overrideStatus != null &&
widget.currentStatus == _overrideStatus) {
setState(() {
_overrideStatus = null;
});
} else if (oldWidget.currentStatus != widget.currentStatus &&
_overrideStatus != null) {
_overrideStatus = null;
}
}
String get _effectiveStatus => _overrideStatus ?? widget.currentStatus;
Future<bool> _onStatusChanged(String newStatus, {String? notes, String? proofImagePath}) async {
debugPrint('[OSR] _onStatusChanged: $newStatus, proofImagePath: $proofImagePath');
if (_isProcessing) return false;
// Optimistic UI: flip status immediately to avoid visible lag.
setState(() {
_isProcessing = true;
_overrideStatus = newStatus;
});
bool success = false;
bool timedOut = false;
try {
// Hard cap wait time to keep UI from spinning indefinitely.
success = await widget
.onStatusChange(newStatus, notes: notes, proofImagePath: proofImagePath)
.timeout(
const Duration(seconds: 3),
onTimeout: () {
timedOut = true;
// Assume success on timeout to avoid UI rollback; data will
// refresh from server on next fetch.
return true;
},
);
} catch (_) {
success = false;
} finally {
if (!mounted) return success;
setState(() {
_isProcessing = false;
// If API failed, revert the optimistic status.
// If timed out, keep the optimistic status (server likely completed).
_overrideStatus = (success || timedOut) ? newStatus : null;
});
}
return success;
}
// ------------------------------
// REJECT SHEET
// ------------------------------
void _showRejectSheet(BuildContext context) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
String selectedReason = "";
bool isLoading = false;
final List<String> reasons = [
"Customer not reachable",
"Wrong address",
"Out of delivery area",
"Other reason",
];
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Reject Order",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 10),
for (String reason in reasons)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: GestureDetector(
onTap: () => setModalState(() {
selectedReason = reason;
}),
child: Container(
height: 55,
width: double.infinity,
decoration: BoxDecoration(
color: selectedReason == reason
? Colors.red
: Colors.grey[300],
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.center,
child: Text(
reason,
style: TextStyle(
color: selectedReason == reason
? Colors.white
: Colors.black,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
),
),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: selectedReason.isEmpty || isLoading
? null
: () async {
setModalState(() => isLoading = true);
final success = await _onStatusChanged(
"REJECTED",
notes: selectedReason,
);
if (context.mounted) {
Navigator.pop(context, success);
}
},
child: isLoading
? const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
)
: const Text(
"Reject Order",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
);
},
),
);
},
);
}
// ------------------------------
// CANCEL SHEET
// ------------------------------
void _showCancelSheet(BuildContext context) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
bool confirmCancel = false;
bool isLoading = false;
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Cancel this order?",
style:
TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
const Text(
"Once cancelled, this order will return to pending state.",
textAlign: TextAlign.center,
),
const SizedBox(height: 25),
CheckboxListTile(
value: confirmCancel,
onChanged: (value) => setModalState(
() => confirmCancel = value ?? false,
),
title: const Text("I confirm to cancel this order"),
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: !confirmCancel
? null
: () async {
setModalState(() => isLoading = true);
await Future.delayed(const Duration(seconds: 1));
if (context.mounted) Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Order Cancelled"),
),
);
},
child: isLoading
? const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
)
: const Text(
"Confirm Cancel",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
);
},
),
);
},
);
}
@override
Widget build(BuildContext context) {
final String currentStatus = _effectiveStatus;
final bool showCancel =
currentStatus == "ARRIVED" || currentStatus == "PICKED";
final String leftText = showCancel ? "CANCEL" : "REJECT";
return Stack(
children: [
Row(
children: [
// LEFT BUTTON (Reject / Cancel)
Expanded(
child: InkWell(
onTap: () {
if (showCancel) {
_showCancelSheet(context);
} else {
_showRejectSheet(context);
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: const BoxDecoration(
color: Colors.red, //
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(12),
),
),
alignment: Alignment.center,
child: Text(
leftText,
style: TextStyle(
fontSize: 19,
color: Colors.white,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
),
),
),
// RIGHT BUTTON (ACCEPT / ARRIVED / PICKED)
OrderStatusButton(
key: ValueKey(currentStatus),
currentStatus: currentStatus,
onStatusChange: (status, {proofImagePath}) =>
_onStatusChanged(status, proofImagePath: proofImagePath),
enabled: widget.enabled && !_isProcessing,
),
],
),
if (_isProcessing)
Positioned.fill(
child: IgnorePointer(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.12),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: const Center(
child: SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
),
),
),
),
),
],
);
}
}
/// ------------------------------
/// STATUS BUTTON LOGIC (NO DELIVERY)
/// ------------------------------
class OrderStatusButton extends StatefulWidget {
final String currentStatus;
final Future<bool> Function(String newStatus, {String? proofImagePath}) onStatusChange;
final bool enabled;
const OrderStatusButton({
super.key,
required this.currentStatus,
required this.onStatusChange,
this.enabled = true,
});
@override
State<OrderStatusButton> createState() => _OrderStatusButtonState();
}
class _OrderStatusButtonState extends State<OrderStatusButton> {
String get _buttonText => widget.currentStatus;
Color _getButtonColor() {
switch (_buttonText) {
case "ARRIVED":
return Colors.orange;
case "PICKED":
return ColorConstants.primaryColor;
default:
return Colors.green;
}
}
void _showStatusSheet() {
if (_buttonText == "PICKED") return; // final stage now
bool isConfirmLoading = false;
// Determine next status (single step)
String? nextStatus;
if (_buttonText == "ACCEPT") {
nextStatus = "ACCEPTED";
} else if (_buttonText == "ACCEPTED") {
nextStatus = "ARRIVED";
} else if (_buttonText == "ARRIVED") {
nextStatus = "PICKED";
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
// If for some reason we don't have a valid next status, show nothing
if (nextStatus == null) {
return const SizedBox.shrink();
}
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Move Your Order to",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
Transform.translate(
offset: const Offset(0, -5),
child: IconButton(
icon: const Icon(
Icons.cancel,
color: Colors.red,
size: 36,
),
onPressed: () => Navigator.pop(context),
),
),
],
),
const SizedBox(height: 20),
// Slider to confirm moving to next status wrapped with SafeArea
SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: SizedBox(
width: double.infinity,
child: SlideToSubmit.custom(
height: 55,
sliderWidth: 40,
padding: const EdgeInsets.all(8),
backgroundDecoration: BoxDecoration(
color: _getButtonColor().withOpacity(0.5),
borderRadius: BorderRadius.circular(40),
),
foregroundDecoration: BoxDecoration(
color: _getButtonColor(),
borderRadius: BorderRadius.circular(999),
),
slider: Center(
child: ClipOval(
child: Container(
height: 40,
width: 40,
color: Colors.white,
padding: const EdgeInsets.all(8),
child: const Icon(
Icons.arrow_forward_ios,
size: 24,
color: Colors.black,
),
),
),
),
hint: Align(
alignment: Alignment.center,
child: Text(
'Slide to mark $nextStatus',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
color: const Color.fromARGB(255, 15, 14, 14),
),
),
),
onSubmit: (controller) async {
if (isConfirmLoading) return;
setModalState(() => isConfirmLoading = true);
// Close sheet after slide completes
if (context.mounted) {
Navigator.pop(context);
}
// Trigger status change callback
WidgetsBinding.instance.addPostFrameCallback((
_,
) async {
if (nextStatus == "PICKED") {
debugPrint('[OSB] Status is PICKED, launching camera...');
// Trigger Camera
final ImagePicker picker = ImagePicker();
final XFile? photo = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 50, // Optimize size
);
if (photo != null) {
debugPrint('[OSB] Photo taken: ${photo.path}');
// Process with image
await widget.onStatusChange(
nextStatus!,
proofImagePath: photo.path,
);
} else {
debugPrint('[OSB] Camera cancelled or photo null');
}
} else {
debugPrint('[OSB] Status NOT PICKED (is $nextStatus), normal flow');
// Normal flow
await widget.onStatusChange(nextStatus!);
}
try {
controller.reset();
} catch (_) {}
});
},
),
),
),
],
),
);
},
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Expanded(
child: InkWell(
onTap: widget.enabled ? _showStatusSheet : null,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: widget.enabled ? _getButtonColor() : Colors.grey,
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(12),
),
),
alignment: Alignment.center,
child: Text(
_buttonText,
style: TextStyle(
fontSize: 19,
color: Colors.white,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
),
),
);
}
}