Files
doormile_milderapp/lib/views/Dashboard/pickups/payment_screen.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 05:40:35 +05:30

859 lines
32 KiB
Dart

import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:miler/views/helpers/constants/Font_constant.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/views/helpers/constants/design_constants.dart';
import 'package:miler/views/helpers/widgets/app_widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:slider_button_lite/feature/presentation/slider_button/slider.dart';
import 'package:slider_button_lite/feature/presentation/slider_button/slider_button_prop.dart';
class PaymentScreen extends StatefulWidget {
final Map<String, dynamic> pickup;
final Map<String, dynamic> verificationData;
const PaymentScreen({
super.key,
required this.pickup,
required this.verificationData,
});
@override
State<PaymentScreen> createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen>
with TickerProviderStateMixin {
String _paymentMethod = 'cash';
bool _isProcessing = false;
bool _isSuccess = false;
bool _paymentVerified = false;
double _pickupCharge = 0.0;
double _totalAmount = 0.0;
late final String _customerName;
late final String _orderId;
late final String _bookingId;
late final String _trackingId;
late final String _weight;
late final String _description;
late AnimationController _successAnimController;
late Animation<double> _successScaleAnim;
late Animation<double> _successFadeAnim;
@override
void initState() {
super.initState();
final d = widget.pickup;
final v = widget.verificationData;
_customerName = (d['pickupcustomer'] ?? 'Customer').toString();
_orderId = (d['orderid'] ?? '').toString();
_bookingId = _orderId.isNotEmpty
? _orderId
: 'DM${DateTime.now().millisecondsSinceEpoch.toString().substring(5)}';
_trackingId =
(d['trackingid'] ??
'TRK${DateTime.now().millisecondsSinceEpoch.toString().substring(4)}')
.toString();
_weight = (v['weight'] ?? '0').toString();
_description = (v['description'] ?? '').toString();
_pickupCharge = double.tryParse(_weight) != null
? double.parse(_weight) * 15.0
: 30.0;
_totalAmount = _pickupCharge;
_successAnimController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_successScaleAnim = CurvedAnimation(
parent: _successAnimController,
curve: Curves.elasticOut,
);
_successFadeAnim = CurvedAnimation(
parent: _successAnimController,
curve: Curves.easeOut,
);
}
@override
void dispose() {
_successAnimController.dispose();
super.dispose();
}
Future<void> _processPayment() async {
if (_isProcessing) return;
setState(() => _isProcessing = true);
// Simulate backend payment verification
await Future.delayed(const Duration(milliseconds: 1500));
if (!mounted) return;
// Verify payment was successful
_paymentVerified = true;
setState(() {
_isProcessing = false;
_isSuccess = true;
});
// Start success animation
_successAnimController.forward();
// Auto-navigate after showing success (optional)
await Future.delayed(const Duration(milliseconds: 2500));
if (mounted && _isSuccess) {
_completePayment();
}
}
void _completePayment({bool goToBookings = false}) {
if (!mounted) return;
Navigator.pop(context, {
'paid': true,
'goto_bookings': goToBookings,
'verified': true,
});
}
@override
Widget build(BuildContext context) {
if (_isSuccess) return _buildSuccessView();
return _buildPaymentView();
}
// ────────────────────────────── SUCCESS VIEW ─────────────────────────────
Widget _buildSuccessView() {
return Scaffold(
backgroundColor: ColorConstants.surface,
body: SafeArea(
child: FadeTransition(
opacity: _successFadeAnim,
child: Padding(
padding: EdgeInsets.all(24.r),
child: Column(
children: [
const Spacer(flex: 2),
// Success animation circle
ScaleTransition(
scale: _successScaleAnim,
child: Container(
width: 110.r,
height: 110.r,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ColorConstants.success,
ColorConstants.acceptGreen,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: ColorConstants.success.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Icon(
LucideIcons.check,
color: Colors.white,
size: 52.sp,
),
),
),
SizedBox(height: 24.h),
Text(
'Payment Collected Successfully',
style: TextStyle(
fontSize: 26.sp,
fontWeight: FontWeight.w800,
color: ColorConstants.onSurface,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: 8.h),
Text(
'₹${_totalAmount.toStringAsFixed(0)} via ${_paymentMethod.toUpperCase()}',
style: TextStyle(
fontSize: 15.sp,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: 32.h),
// Receipt Card - Corporate Design
Container(
width: double.infinity,
decoration: BoxDecoration(
color: ColorConstants.pureSurface,
borderRadius: BorderRadius.circular(
DesignConstants.radiusXl,
),
border: Border.all(color: ColorConstants.outlineVariant),
boxShadow: DesignConstants.shadowSm,
),
child: Column(
children: [
// Receipt header
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ColorConstants.primary.withValues(alpha: 0.08),
ColorConstants.primary.withValues(alpha: 0.03),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.vertical(
top: Radius.circular(18.r),
),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(10.r),
decoration: BoxDecoration(
color: ColorConstants.primaryContainer
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(
DesignConstants.radiusLg,
),
),
child: Icon(
LucideIcons.receiptText,
color: ColorConstants.primary,
size: 18.sp,
),
),
SizedBox(width: 10.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Payment Receipt',
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.onSurface,
fontFamily: FontConstants.fontFamily,
),
),
Text(
'Payment verified and recorded',
style: TextStyle(
fontSize: 12.sp,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
],
),
),
Padding(
padding: EdgeInsets.all(16.r),
child: Column(
children: [
_receiptRow('Booking ID', '#$_bookingId'),
SizedBox(height: 10.h),
Divider(
color: ColorConstants.outlineVariant,
height: 1,
),
SizedBox(height: 10.h),
_receiptRow('Tracking ID', _trackingId),
SizedBox(height: 10.h),
Divider(
color: ColorConstants.outlineVariant,
height: 1,
),
SizedBox(height: 10.h),
_receiptRow('Customer', _customerName),
SizedBox(height: 10.h),
Divider(
color: ColorConstants.outlineVariant,
height: 1,
),
SizedBox(height: 10.h),
_receiptRow(
'Amount Collected',
'₹${_totalAmount.toStringAsFixed(0)}',
valueColor: ColorConstants.success,
valueBold: true,
),
],
),
),
],
),
),
const Spacer(flex: 2),
// Action Buttons
Column(
children: [
// Next Booking Button
MilerButton(
label: 'Move to next booking',
onPressed: () => _completePayment(goToBookings: true),
),
SizedBox(height: 12.h),
TextButton(
onPressed: () => _completePayment(goToBookings: false),
child: Text(
'Return to Collection',
style: TextStyle(
fontSize: 14.sp,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
),
),
],
),
SizedBox(height: 16.h),
],
),
),
),
),
);
}
Widget _receiptRow(
String label,
String value, {
Color? valueColor,
bool valueBold = false,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 13.sp,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
),
Text(
value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: valueBold ? FontWeight.w600 : FontWeight.w500,
color: valueColor ?? ColorConstants.onSurface,
fontFamily: FontConstants.fontFamily,
),
),
],
);
}
// ────────────────────────────── PAYMENT VIEW ─────────────────────────────
Widget _buildPaymentView() {
return Scaffold(
backgroundColor: ColorConstants.surface,
body: Column(
children: [
// ── Premium Gradient Header ──────────────────────────────────
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ColorConstants.primary,
ColorConstants.primaryContainer,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
bottom: false,
child: Column(
children: [
// App bar row
Padding(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 8.h,
),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: ColorConstants.pureSurface.withValues(
alpha: 0.15,
),
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.chevronLeft,
color: Colors.white,
size: 16.sp,
),
),
),
SizedBox(width: 12.w),
Text(
'Payment Collection',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w700,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
letterSpacing: 0.5,
),
),
],
),
),
// Amount display
Padding(
padding: EdgeInsets.fromLTRB(20.w, 8.h, 20.w, 24.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Amount to Collect',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white.withValues(alpha: 0.75),
fontFamily: FontConstants.fontFamily,
letterSpacing: 0.3,
),
),
SizedBox(height: 4.h),
Text(
'₹${_totalAmount.toStringAsFixed(0)}',
style: TextStyle(
fontSize: 36.sp,
fontWeight: FontWeight.w800,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
letterSpacing: -1,
),
),
],
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: ColorConstants.pureSurface.withValues(
alpha: 0.15,
),
borderRadius: BorderRadius.circular(
DesignConstants.radiusLg,
),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'#$_orderId',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
),
),
Text(
_customerName,
style: TextStyle(
fontSize: 11.sp,
color: Colors.white.withValues(alpha: 0.8),
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
],
),
),
],
),
),
),
// ── Scrollable body ──────────────────────────────────────────
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.all(16.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 4.h),
// ── Booking Summary Card ──────────────────────────────
_sectionLabel('Booking Summary'),
SizedBox(height: 10.h),
Container(
width: double.infinity,
padding: EdgeInsets.all(16.r),
decoration: _cardDecoration(),
child: Column(
children: [
_summaryRow('Customer', _customerName),
_divider(),
_summaryRow('Booking', '#$_orderId'),
_divider(),
_summaryRow('Weight', '$_weight kg'),
if (_description.isNotEmpty) ...[
_divider(),
_summaryRow('Item', _description),
],
_divider(),
_summaryRow(
'Collection Charge',
'₹${_pickupCharge.toStringAsFixed(2)}',
isBold: true,
),
Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Container(
height: 1,
color: ColorConstants.primary.withValues(
alpha: 0.3,
),
),
),
_summaryRow(
'Total',
'₹${_totalAmount.toStringAsFixed(2)}',
isBold: true,
isTotal: true,
),
],
),
),
SizedBox(height: 20.h),
// ── Payment Method ──────────────────────────────────
_sectionLabel('Payment Method'),
SizedBox(height: 10.h),
_buildPaymentOption(
LucideIcons.banknote,
'Cash',
'Pay with cash on collection',
'cash',
ColorConstants.success,
),
SizedBox(height: 10.h),
_buildPaymentOption(
LucideIcons.scanLine,
'QR / UPI',
'Scan QR code to pay',
'upi',
ColorConstants.upiPurple,
),
SizedBox(height: 10.h),
_buildPaymentOption(
LucideIcons.creditCard,
'Card',
'Swipe / tap card terminal',
'card',
ColorConstants.tertiary,
),
SizedBox(height: 28.h),
// ── Slide to Collect Button ─────────────────────────
_isProcessing
? Container(
height: ButtonSizes.primary,
decoration: BoxDecoration(
color: ColorConstants.primary,
borderRadius: BorderRadius.circular(
DesignConstants.radiusXl,
),
),
child: const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(
DesignConstants.radiusXl,
),
child: Container(
color: ColorConstants.primary,
child: SliderButton(
properties: SliderButtonProperties(
radius: 24,
height: ButtonSizes.primary,
buttonSize: 48,
width: MediaQuery.of(context).size.width - 32.w,
backgroundColor: Colors.transparent,
dismissThresholds: 0.85,
action: () async {
debugPrint(
'[PAYMENT_SLIDER] Action called, processing payment',
);
await _processPayment();
debugPrint(
'[PAYMENT_SLIDER] _processPayment completed, returning true',
);
return true;
},
label: Text(
'Slide to Collect ₹${_totalAmount.toStringAsFixed(0)}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
fontFamily: FontConstants.fontFamily,
),
),
icon: Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: ColorConstants.pureSurface,
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.banknote,
size: 22.sp,
color: ColorConstants.primary,
),
),
),
),
),
),
SizedBox(height: 10.h),
Center(
child: Text(
'Collect from customer before starting pickup',
style: TextStyle(
fontSize: 12.sp,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
),
),
SizedBox(height: 20.h),
],
),
),
),
],
),
);
}
Widget _sectionLabel(String label) {
return Text(
label,
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
letterSpacing: 0.5,
),
);
}
Widget _buildPaymentOption(
IconData icon,
String title,
String subtitle,
String value,
Color accentColor,
) {
final isSelected = _paymentMethod == value;
return GestureDetector(
onTap: () => setState(() => _paymentMethod = value),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: double.infinity,
padding: EdgeInsets.all(14.r),
decoration: BoxDecoration(
color: isSelected
? accentColor.withValues(alpha: 0.06)
: ColorConstants.pureSurface,
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
border: Border.all(
color: isSelected ? accentColor : ColorConstants.outlineVariant,
width: isSelected ? 2 : 1,
),
boxShadow: isSelected
? [
BoxShadow(
color: accentColor.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: [],
),
child: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: EdgeInsets.all(10.r),
decoration: BoxDecoration(
color: isSelected
? accentColor.withValues(alpha: 0.12)
: const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
),
child: Icon(
icon,
color: isSelected
? accentColor
: ColorConstants.onSurfaceVariant,
size: 22.sp,
),
),
SizedBox(width: 14.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.onSurface,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: 2.h),
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: ColorConstants.onSurfaceVariant,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 22.w,
height: 22.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? accentColor : Colors.transparent,
border: Border.all(
color: isSelected ? accentColor : const Color(0xFFD1D5DB),
width: isSelected ? 0 : 2,
),
),
child: isSelected
? Icon(LucideIcons.check, color: Colors.white, size: 14.sp)
: null,
),
],
),
),
);
}
Widget _summaryRow(
String label,
String value, {
bool isBold = false,
bool isTotal = false,
}) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 5.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: isTotal ? 15.sp : 13.sp,
color: ColorConstants.onSurfaceVariant,
fontWeight: isBold ? FontWeight.w500 : FontWeight.normal,
fontFamily: FontConstants.fontFamily,
),
),
Text(
value,
style: TextStyle(
fontSize: isTotal ? 17.sp : 14.sp,
fontWeight: isTotal
? FontWeight.w700
: (isBold ? FontWeight.w600 : FontWeight.w500),
color: isTotal
? ColorConstants.primary
: ColorConstants.onSurface,
fontFamily: FontConstants.fontFamily,
),
),
],
),
);
}
Widget _divider() => Padding(
padding: EdgeInsets.symmetric(vertical: 2.h),
child: Divider(color: ColorConstants.outlineVariant, height: 1),
);
BoxDecoration _cardDecoration() {
return BoxDecoration(
color: ColorConstants.pureSurface,
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
border: Border.all(color: ColorConstants.outlineVariant),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
);
}
}