initial commit: push everything
This commit is contained in:
611
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
611
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
@@ -0,0 +1,611 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user