910 lines
30 KiB
Dart
910 lines
30 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
import 'package:image_picker/image_picker.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:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:miler/views/Dashboard/pickups/stop_type.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 PickupVerificationPage extends StatefulWidget {
|
|
final Map<String, dynamic> pickup;
|
|
const PickupVerificationPage({super.key, required this.pickup});
|
|
|
|
@override
|
|
State<PickupVerificationPage> createState() => _PickupVerificationPageState();
|
|
}
|
|
|
|
class _PickupVerificationPageState extends State<PickupVerificationPage>
|
|
with TickerProviderStateMixin {
|
|
File? _parcelImage;
|
|
final ImagePicker _picker = ImagePicker();
|
|
bool _isConfirming = false;
|
|
bool _photoVerified = false;
|
|
|
|
// Form controllers
|
|
final TextEditingController _weightController = TextEditingController();
|
|
final TextEditingController _descriptionController = TextEditingController();
|
|
final TextEditingController _otpController = TextEditingController();
|
|
final TextEditingController _dropAddressController = TextEditingController();
|
|
|
|
late final String _customerName;
|
|
late final String _pickupAddress;
|
|
late final String _orderId;
|
|
late final String _dropAddressDefault;
|
|
|
|
// Delivery stops (Amazon/Flipkart-style) verify differently: proof is
|
|
// OTP + photo (prepaid, no weight/signature). Pickups keep the full form.
|
|
late final bool _isDelivery;
|
|
|
|
late AnimationController _fadeController;
|
|
late Animation<double> _fadeAnim;
|
|
|
|
// Red Theme Colors
|
|
static Color get _primaryRed => ColorConstants.primary;
|
|
static Color get _darkRed => ColorConstants.primary;
|
|
static Color get _lightRed => ColorConstants.errorLight;
|
|
static Color get _successGreen => ColorConstants.acceptGreen;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final d = widget.pickup;
|
|
_customerName = (d['PickupCustomer'] ?? d['pickupcustomer'] ?? 'Customer')
|
|
.toString();
|
|
_pickupAddress =
|
|
(d['pickupaddress'] ?? d['PickupAddress'] ?? 'Pickup Location')
|
|
.toString();
|
|
_orderId = (d['orderid'] ?? '').toString();
|
|
_dropAddressDefault =
|
|
(d['DropAddress'] ?? d['dropaddress'] ?? 'Drop Location').toString();
|
|
_dropAddressController.text = _dropAddressDefault;
|
|
_isDelivery = stopKindOf(d).isDelivery;
|
|
|
|
_fadeController = AnimationController(
|
|
duration: const Duration(milliseconds: 500),
|
|
vsync: this,
|
|
);
|
|
_fadeAnim = CurvedAnimation(parent: _fadeController, curve: Curves.easeOut);
|
|
_fadeController.forward();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_weightController.dispose();
|
|
_descriptionController.dispose();
|
|
_otpController.dispose();
|
|
_dropAddressController.dispose();
|
|
_fadeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _takeParcelPhoto() async {
|
|
try {
|
|
final XFile? photo = await _picker.pickImage(
|
|
source: ImageSource.camera,
|
|
imageQuality: 85,
|
|
);
|
|
if (photo != null && mounted) {
|
|
setState(() {
|
|
_parcelImage = File(photo.path);
|
|
_photoVerified = true;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Failed to capture photo')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Required proof per stop is kept light: Photo + Code (pickup) / Photo + OTP
|
|
// (delivery). Weight is optional — no keyboard entry needed at every doorstep.
|
|
bool get _isFormValid => _isDelivery
|
|
? (_photoVerified && _otpController.text.trim().isNotEmpty)
|
|
: (_photoVerified && _otpController.text.trim().isNotEmpty);
|
|
|
|
void _confirmPickup() {
|
|
if (!_isFormValid) {
|
|
String msg = 'Please complete all required fields';
|
|
if (!_photoVerified) {
|
|
msg = _isDelivery
|
|
? 'Take a photo of the delivered parcel first'
|
|
: 'Take a photo of the parcel first';
|
|
} else if (_isDelivery && _otpController.text.trim().isEmpty) {
|
|
msg = 'Enter the delivery OTP from the customer';
|
|
} else if (!_isDelivery && _otpController.text.trim().isEmpty) {
|
|
msg = 'Enter the pickup code from the customer';
|
|
}
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(msg),
|
|
backgroundColor: _primaryRed,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() => _isConfirming = true);
|
|
|
|
final verified = {
|
|
'verified': true,
|
|
'parcelImage': _parcelImage?.path,
|
|
'weight': _weightController.text.trim(),
|
|
'description': _descriptionController.text.trim(),
|
|
'dropAddress': _dropAddressController.text.trim(),
|
|
'otp': _otpController.text.trim(),
|
|
};
|
|
|
|
debugPrint(
|
|
'[PICKUP_VERIFY] Confirming pickup and popping with verified data',
|
|
);
|
|
|
|
// Pop with verified data
|
|
Navigator.pop(context, verified);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: ColorConstants.daylightSurface,
|
|
appBar: _buildAppBar(),
|
|
body: FadeTransition(
|
|
opacity: _fadeAnim,
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 32.h),
|
|
child: SizedBox(
|
|
width: MediaQuery.of(context).size.width - 32.w,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildProgressSteps(),
|
|
SizedBox(height: 20.h),
|
|
|
|
_buildCustomerCard(),
|
|
SizedBox(height: 20.h),
|
|
|
|
_buildSectionHeader(
|
|
icon: LucideIcons.camera,
|
|
title: _isDelivery ? 'Delivery Photo' : 'Parcel Photo',
|
|
badge: 'Required',
|
|
isComplete: _photoVerified,
|
|
),
|
|
SizedBox(height: 10.h),
|
|
_buildPhotoCapture(),
|
|
SizedBox(height: 20.h),
|
|
|
|
// Weight is pickup-only. Deliveries are prepaid and proven by
|
|
// OTP + photo. Signature was dropped — photo is enough proof for
|
|
// a first-mile pickup, so it was pure friction on every stop.
|
|
if (!_isDelivery) ...[
|
|
_buildSectionHeader(
|
|
icon: LucideIcons.package,
|
|
title: 'Parcel Details (Optional)',
|
|
isComplete: _weightController.text.isNotEmpty,
|
|
),
|
|
SizedBox(height: 10.h),
|
|
_buildParcelDetailsCard(),
|
|
SizedBox(height: 20.h),
|
|
],
|
|
|
|
_buildSectionHeader(
|
|
icon: LucideIcons.lock,
|
|
title: _isDelivery ? 'Delivery OTP' : 'Pickup Code',
|
|
badge: 'Required',
|
|
isComplete: _otpController.text.isNotEmpty,
|
|
),
|
|
SizedBox(height: 10.h),
|
|
_buildCard([
|
|
TextField(
|
|
controller: _otpController,
|
|
keyboardType: TextInputType.number,
|
|
maxLength: 6,
|
|
onChanged: (_) => setState(() {}),
|
|
decoration:
|
|
_inputDecoration(
|
|
_isDelivery
|
|
? 'Enter OTP from customer'
|
|
: 'Enter code from customer',
|
|
).copyWith(
|
|
counterText: '',
|
|
prefixIcon: Icon(
|
|
LucideIcons.lock,
|
|
color: ColorConstants.secondaryText,
|
|
size: 20.sp,
|
|
),
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
]),
|
|
SizedBox(height: 32.h),
|
|
|
|
_buildConfirmSlider(),
|
|
SizedBox(height: 20.h),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
AppBar _buildAppBar() {
|
|
return AppBar(
|
|
backgroundColor: ColorConstants.pureSurface,
|
|
elevation: 0,
|
|
centerTitle: false,
|
|
surfaceTintColor: Colors.transparent,
|
|
// 56 → 68, the same air the tab bars gained. Height rather than a title
|
|
// inset so the back arrow moves down with the heading instead of the two
|
|
// drifting apart.
|
|
toolbarHeight: 68,
|
|
leading: IconButton(
|
|
tooltip: 'Back',
|
|
icon: Icon(
|
|
LucideIcons.chevronLeft,
|
|
color: ColorConstants.onSurface,
|
|
size: 20.sp,
|
|
),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
title: Row(
|
|
children: [
|
|
// No brand mark. It is gone from every bar in the app: on a pushed
|
|
// sub-page the back arrow beside it already says where the rider is,
|
|
// and the logo was repeating what the screen he came from said.
|
|
Text(
|
|
_isDelivery ? 'Verify Delivery' : 'Verify Pickup',
|
|
style: TextStyle(
|
|
fontSize: 21.sp,
|
|
color: ColorConstants.onSurface,
|
|
fontWeight: FontWeight.w800,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(1),
|
|
child: Container(height: 1, color: ColorConstants.dividerColor),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProgressSteps() {
|
|
final steps = _isDelivery
|
|
? [
|
|
{'label': 'Photo', 'done': _photoVerified},
|
|
{'label': 'OTP', 'done': _otpController.text.isNotEmpty},
|
|
]
|
|
: [
|
|
{'label': 'Photo', 'done': _photoVerified},
|
|
{'label': 'Code', 'done': _otpController.text.isNotEmpty},
|
|
];
|
|
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 12.h),
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.pureSurface,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
|
border: Border.all(color: ColorConstants.dividerColor),
|
|
),
|
|
child: Row(
|
|
children: List.generate(steps.length * 2 - 1, (i) {
|
|
if (i.isOdd) {
|
|
final leftDone = steps[i ~/ 2]['done'] as bool;
|
|
return Expanded(
|
|
child: Container(
|
|
height: 2.h,
|
|
margin: EdgeInsets.symmetric(horizontal: 4.w),
|
|
decoration: BoxDecoration(
|
|
color: leftDone ? _successGreen : ColorConstants.dividerColor,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
final idx = i ~/ 2;
|
|
final step = steps[idx];
|
|
final done = step['done'] as bool;
|
|
final label = step['label'] as String;
|
|
return Expanded(
|
|
child: Column(
|
|
children: [
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
width: 32.w,
|
|
height: 32.w,
|
|
decoration: BoxDecoration(
|
|
color: done ? _successGreen : ColorConstants.neutralLight,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: done ? _successGreen : ColorConstants.dividerColor,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: Icon(
|
|
done ? LucideIcons.check : LucideIcons.circle,
|
|
color: done ? Colors.white : ColorConstants.secondaryText,
|
|
size: 16.sp,
|
|
),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 10.sp,
|
|
fontWeight: done ? FontWeight.w700 : FontWeight.w600,
|
|
color: done
|
|
? _successGreen
|
|
: ColorConstants.onSurfaceVariant,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCustomerCard() {
|
|
final initials = _customerName.trim().isNotEmpty
|
|
? _customerName
|
|
.trim()
|
|
.split(' ')
|
|
.map((w) => w.isNotEmpty ? w[0] : '')
|
|
.take(2)
|
|
.join()
|
|
.toUpperCase()
|
|
: 'C';
|
|
|
|
return Container(
|
|
padding: EdgeInsets.all(16.r),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [_primaryRed, _darkRed],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: _primaryRed.withValues(alpha: 0.25),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 44.w,
|
|
height: 44.w,
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.pureSurface.withValues(alpha: 0.2),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: Colors.white.withValues(alpha: 0.4),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
initials,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w800,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_customerName,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.white,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
SizedBox(height: 2.h),
|
|
Text(
|
|
'Booking #$_orderId',
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.white.withValues(alpha: 0.8),
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12.h),
|
|
Container(
|
|
height: 1,
|
|
color: ColorConstants.pureSurface.withValues(alpha: 0.2),
|
|
),
|
|
SizedBox(height: 12.h),
|
|
_buildLocationRow(
|
|
LucideIcons.mapPin,
|
|
_pickupAddress,
|
|
_isDelivery ? 'DELIVERY' : 'PICKUP',
|
|
ColorConstants.warning.withValues(alpha: 0.40),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLocationRow(
|
|
IconData icon,
|
|
String address,
|
|
String label,
|
|
Color accentColor,
|
|
) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(5.r),
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.pureSurface.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
),
|
|
child: Icon(icon, size: 14.sp, color: accentColor),
|
|
),
|
|
SizedBox(width: 10.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 9.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: accentColor,
|
|
letterSpacing: 0.8,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
SizedBox(height: 1.h),
|
|
Text(
|
|
address,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.white.withValues(alpha: 0.9),
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHeader({
|
|
required IconData icon,
|
|
required String title,
|
|
String? badge,
|
|
bool isComplete = false,
|
|
}) {
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(7.r),
|
|
decoration: BoxDecoration(
|
|
color: isComplete
|
|
? ColorConstants.successLight
|
|
: ColorConstants.neutralLight,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
),
|
|
child: Icon(
|
|
isComplete ? LucideIcons.circleCheck : icon,
|
|
size: 16.sp,
|
|
color: isComplete ? _successGreen : ColorConstants.onSurfaceVariant,
|
|
),
|
|
),
|
|
SizedBox(width: 10.w),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: ColorConstants.onSurface,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
if (badge != null)
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 3.h),
|
|
decoration: BoxDecoration(
|
|
color: _lightRed,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
),
|
|
child: Text(
|
|
badge,
|
|
style: TextStyle(
|
|
fontSize: 10.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: _primaryRed,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPhotoCapture() {
|
|
return GestureDetector(
|
|
onTap: _takeParcelPhoto,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
width: double.infinity,
|
|
height: 180.h,
|
|
decoration: BoxDecoration(
|
|
color: _photoVerified
|
|
? Colors.transparent
|
|
: ColorConstants.pureSurface,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
|
border: Border.all(
|
|
color: _photoVerified ? _successGreen : ColorConstants.borderSubtle,
|
|
width: _photoVerified ? 2 : 1,
|
|
),
|
|
boxShadow: _photoVerified
|
|
? [
|
|
BoxShadow(
|
|
color: _successGreen.withValues(alpha: 0.1),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
]
|
|
: [],
|
|
),
|
|
child: _parcelImage != null
|
|
? ClipRRect(
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
Image.file(_parcelImage!, fit: BoxFit.cover),
|
|
Positioned(
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 14.w,
|
|
vertical: 10.h,
|
|
),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.bottomCenter,
|
|
end: Alignment.topCenter,
|
|
colors: [
|
|
Colors.black.withValues(alpha: 0.6),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
LucideIcons.circleCheck,
|
|
color: Colors.white,
|
|
size: 16.sp,
|
|
),
|
|
SizedBox(width: 6.w),
|
|
Text(
|
|
'Photo captured — tap to retake',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12.sp,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 10.h,
|
|
right: 10.w,
|
|
child: Container(
|
|
padding: EdgeInsets.all(6.r),
|
|
decoration: BoxDecoration(
|
|
color: _successGreen,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
LucideIcons.check,
|
|
color: Colors.white,
|
|
size: 14.sp,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(18.r),
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.neutralLight,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
LucideIcons.camera,
|
|
color: ColorConstants.secondaryText,
|
|
size: 36.sp,
|
|
),
|
|
),
|
|
SizedBox(height: 10.h),
|
|
Text(
|
|
'Tap to take parcel photo',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: ColorConstants.onSurfaceVariant,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
'Required for pickup confirmation',
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildParcelDetailsCard() {
|
|
return _buildCard([
|
|
_fieldLabel('Weight (kg)'),
|
|
SizedBox(height: 6.h),
|
|
TextField(
|
|
controller: _weightController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
onChanged: (_) => setState(() {}),
|
|
decoration: _inputDecoration('e.g. 2.5').copyWith(
|
|
suffixText: 'kg',
|
|
suffixStyle: TextStyle(
|
|
fontSize: 13.sp,
|
|
color: ColorConstants.onSurfaceVariant,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
style: TextStyle(fontSize: 15.sp, fontFamily: FontConstants.fontFamily),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
|
|
_fieldLabel('Description (Optional)'),
|
|
SizedBox(height: 6.h),
|
|
TextField(
|
|
controller: _descriptionController,
|
|
maxLines: 3,
|
|
decoration: _inputDecoration('Item description, fragile, etc.'),
|
|
style: TextStyle(fontSize: 15.sp, fontFamily: FontConstants.fontFamily),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
]);
|
|
}
|
|
|
|
Widget _fieldLabel(String label, {bool required = false}) {
|
|
return Row(
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: ColorConstants.onSurface,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
if (required) ...[
|
|
SizedBox(width: 4.w),
|
|
Text(
|
|
'*',
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
color: _primaryRed,
|
|
fontWeight: FontWeight.w700,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildConfirmSlider() {
|
|
if (_isConfirming) {
|
|
return Container(
|
|
height: ButtonSizes.primary,
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.primary,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
|
),
|
|
child: const Center(
|
|
child: SizedBox(
|
|
width: 22,
|
|
height: 22,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (!_isFormValid) {
|
|
return Container(
|
|
height: ButtonSizes.primary,
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.dividerColor,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
LucideIcons.lock,
|
|
color: ColorConstants.secondaryText,
|
|
size: 18.sp,
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
!_photoVerified
|
|
? 'Take a photo to continue'
|
|
: _otpController.text.isEmpty
|
|
? (_isDelivery
|
|
? 'Enter OTP to continue'
|
|
: 'Enter pickup code to continue')
|
|
: 'Complete all fields to continue',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return SliderButton(
|
|
properties: SliderButtonProperties(
|
|
height: ButtonSizes.primary,
|
|
buttonSize: 48,
|
|
width: MediaQuery.of(context).size.width - 32.w,
|
|
backgroundColor: ColorConstants.primary,
|
|
dismissThresholds: 0.85,
|
|
action: () async {
|
|
debugPrint(
|
|
'[PICKUP_VERIFY_SLIDER] Action called, form valid: $_isFormValid',
|
|
);
|
|
if (!_isFormValid) {
|
|
debugPrint(
|
|
'[PICKUP_VERIFY_SLIDER] Form not valid, returning false',
|
|
);
|
|
return false;
|
|
}
|
|
_confirmPickup();
|
|
debugPrint(
|
|
'[PICKUP_VERIFY_SLIDER] _confirmPickup called, returning true',
|
|
);
|
|
return true;
|
|
},
|
|
label: Text(
|
|
'Slide to Confirm Pickup',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
icon: Container(
|
|
margin: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.pureSurface,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
LucideIcons.check,
|
|
size: 22.sp,
|
|
color: ColorConstants.primary,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCard(List<Widget> children) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.all(16.r),
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.pureSurface,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
|
border: Border.all(color: ColorConstants.borderSubtle),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: children,
|
|
),
|
|
);
|
|
}
|
|
|
|
InputDecoration _inputDecoration(String hint) {
|
|
return InputDecoration(
|
|
hintText: hint,
|
|
hintStyle: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
filled: true,
|
|
fillColor: ColorConstants.daylightSurface,
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 12.h),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
borderSide: BorderSide(color: ColorConstants.borderStrong),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
borderSide: BorderSide(color: ColorConstants.borderStrong),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
borderSide: BorderSide(color: ColorConstants.primary, width: 1.5),
|
|
),
|
|
);
|
|
}
|
|
}
|