612 lines
22 KiB
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,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|