first commit
This commit is contained in:
908
lib/views/Dashboard/pickups/pickup_verify.dart
Normal file
908
lib/views/Dashboard/pickups/pickup_verify.dart
Normal file
@@ -0,0 +1,908 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.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: Icons.camera_alt_rounded,
|
||||
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: Icons.inventory_2_rounded,
|
||||
title: 'Parcel Details (Optional)',
|
||||
isComplete: _weightController.text.isNotEmpty,
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
_buildParcelDetailsCard(),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
|
||||
_buildSectionHeader(
|
||||
icon: Icons.lock_outline_rounded,
|
||||
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(
|
||||
Icons.lock_outline_rounded,
|
||||
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(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
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 ? Icons.check_rounded : Icons.circle_outlined,
|
||||
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.w500,
|
||||
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(
|
||||
Icons.location_on_rounded,
|
||||
_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 ? Icons.check_circle_rounded : 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(
|
||||
Icons.check_circle_rounded,
|
||||
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(
|
||||
Icons.check_rounded,
|
||||
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(
|
||||
Icons.camera_alt_rounded,
|
||||
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(
|
||||
Icons.lock_outline_rounded,
|
||||
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(
|
||||
Icons.check_rounded,
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user