Update project
This commit is contained in:
744
lib/features/orders/presentation/order_tracking.dart
Normal file
744
lib/features/orders/presentation/order_tracking.dart
Normal file
@@ -0,0 +1,744 @@
|
||||
// delivery_tracking_page.dart
|
||||
//
|
||||
// SETUP:
|
||||
// 1. pubspec.yaml -> add:
|
||||
// google_maps_flutter: ^2.9.0
|
||||
// url_launcher: ^6.3.0 (for calling the delivery partner)
|
||||
//
|
||||
// 2. Android -> android/app/src/main/AndroidManifest.xml, inside <application>:
|
||||
// <meta-data
|
||||
// android:name="com.google.android.geo.API_KEY"
|
||||
// android:value="xxx" />
|
||||
//
|
||||
// 3. iOS -> ios/Runner/AppDelegate.swift, inside application(didFinishLaunchingWithOptions):
|
||||
// GMSServices.provideAPIKey("xxx")
|
||||
//
|
||||
// Replace "xxx" above with your real Google Maps API key in BOTH places.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../widgets/text_widget.dart';
|
||||
|
||||
/// Draws a small circular badge marker at runtime (no external icon assets
|
||||
/// needed) — a colored circle with an icon inside, similar to Swiggy's
|
||||
/// bike / restaurant / home pins.
|
||||
Future<BitmapDescriptor> _drawBadgeMarker({
|
||||
required IconData icon,
|
||||
required Color background,
|
||||
Color iconColor = Colors.white,
|
||||
double size = 60,
|
||||
}) async {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
final paint = Paint()..color = background;
|
||||
final radius = size / 2;
|
||||
|
||||
canvas.drawCircle(Offset(radius, radius), radius, Paint()..color = Colors.white);
|
||||
canvas.drawCircle(Offset(radius, radius), radius - 6, paint);
|
||||
|
||||
final textPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||
textPainter.text = TextSpan(
|
||||
text: String.fromCharCode(icon.codePoint),
|
||||
style: TextStyle(
|
||||
fontSize: size * 0.5,
|
||||
fontFamily: icon.fontFamily,
|
||||
package: icon.fontPackage,
|
||||
color: iconColor,
|
||||
),
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(radius - textPainter.width / 2, radius - textPainter.height / 2),
|
||||
);
|
||||
|
||||
final picture = recorder.endRecording();
|
||||
final img = await picture.toImage(size.toInt(), size.toInt());
|
||||
final bytes = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||
return BitmapDescriptor.fromBytes(bytes!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
/// Your Google Maps API key (also enable it for "Directions API").
|
||||
const String kGoogleApiKey = "AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q";
|
||||
|
||||
/// Custom map style JSON — a clean, low-saturation grey theme.
|
||||
const String kSwiggyLikeMapStyle = '''
|
||||
[
|
||||
{"elementType": "geometry", "stylers": [{"color": "#f5f5f3"}]},
|
||||
{"elementType": "labels.icon", "stylers": [{"visibility": "off"}]},
|
||||
{"elementType": "labels.text.fill", "stylers": [{"color": "#8a8a8a"}]},
|
||||
{"elementType": "labels.text.stroke", "stylers": [{"color": "#f5f5f3"}]},
|
||||
{"featureType": "administrative", "elementType": "geometry", "stylers": [{"visibility": "off"}]},
|
||||
{"featureType": "administrative.land_parcel", "stylers": [{"visibility": "off"}]},
|
||||
{"featureType": "administrative.neighborhood", "stylers": [{"visibility": "off"}]},
|
||||
{"featureType": "poi", "stylers": [{"visibility": "off"}]},
|
||||
{"featureType": "poi.park", "elementType": "geometry", "stylers": [{"color": "#e5e8e0"}]},
|
||||
{"featureType": "road", "elementType": "geometry", "stylers": [{"color": "#ffffff"}]},
|
||||
{"featureType": "road", "elementType": "geometry.stroke", "stylers": [{"color": "#e3e3e3"}]},
|
||||
{"featureType": "road.arterial", "elementType": "labels", "stylers": [{"visibility": "off"}]},
|
||||
{"featureType": "road.highway", "elementType": "geometry", "stylers": [{"color": "#f2c9a0"}]},
|
||||
{"featureType": "road.local", "elementType": "labels", "stylers": [{"visibility": "off"}]},
|
||||
{"featureType": "transit", "stylers": [{"visibility": "off"}]},
|
||||
{"featureType": "water", "elementType": "geometry", "stylers": [{"color": "#c9d6df"}]}
|
||||
]
|
||||
''';
|
||||
|
||||
class DeliveryTrackingPage extends StatefulWidget {
|
||||
const DeliveryTrackingPage({super.key});
|
||||
|
||||
@override
|
||||
State<DeliveryTrackingPage> createState() => _DeliveryTrackingPageState();
|
||||
}
|
||||
|
||||
class _DeliveryTrackingPageState extends State<DeliveryTrackingPage> {
|
||||
GoogleMapController? _mapController;
|
||||
|
||||
String _partnerName = 'Majid Khan';
|
||||
double _partnerRating = 4.8;
|
||||
int _partnerOrders = 1240;
|
||||
|
||||
// Sample coordinates - replace with your real pickup / drop / rider coords
|
||||
final LatLng _pickup = const LatLng(28.6450, 77.3550); // restaurant
|
||||
final LatLng _drop = const LatLng(28.6395, 77.3480); // customer
|
||||
LatLng _riderPosition = const LatLng(28.6440, 77.3520);
|
||||
|
||||
final Set<Marker> _markers = {};
|
||||
final Set<Polyline> _polylines = {};
|
||||
List<LatLng> _routePoints = [];
|
||||
|
||||
Timer? _riderTimer;
|
||||
int _routeIndex = 0;
|
||||
int _etaMinutes = 2;
|
||||
|
||||
BitmapDescriptor? _pickupIcon;
|
||||
BitmapDescriptor? _dropIcon;
|
||||
BitmapDescriptor? _riderIcon;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadIcons();
|
||||
}
|
||||
|
||||
Future<void> _loadIcons() async {
|
||||
_pickupIcon = await _drawBadgeMarker(
|
||||
icon: Icons.storefront,
|
||||
background: const Color(0xFFFF6A00),
|
||||
);
|
||||
_dropIcon = await _drawBadgeMarker(
|
||||
icon: Icons.home,
|
||||
background: const Color(0xFF7A1F5C),
|
||||
);
|
||||
_riderIcon = await _drawBadgeMarker(
|
||||
icon: Icons.two_wheeler,
|
||||
background: const Color(0xFF662582),
|
||||
size: 60,
|
||||
);
|
||||
_setupMarkers();
|
||||
_fetchRoute();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_riderTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setupMarkers() {
|
||||
_markers
|
||||
..clear()
|
||||
..addAll([
|
||||
Marker(
|
||||
markerId: const MarkerId('pickup'),
|
||||
position: _pickup,
|
||||
icon: _pickupIcon ??
|
||||
BitmapDescriptor.defaultMarkerWithHue(
|
||||
BitmapDescriptor.hueOrange),
|
||||
infoWindow: const InfoWindow(title: 'Wrapperz'),
|
||||
),
|
||||
Marker(
|
||||
markerId: const MarkerId('drop'),
|
||||
position: _drop,
|
||||
icon: _dropIcon ??
|
||||
BitmapDescriptor.defaultMarkerWithHue(
|
||||
BitmapDescriptor.hueViolet),
|
||||
infoWindow: const InfoWindow(title: 'Friends and Family'),
|
||||
),
|
||||
Marker(
|
||||
markerId: const MarkerId('rider'),
|
||||
position: _riderPosition,
|
||||
icon: _riderIcon ??
|
||||
BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
|
||||
infoWindow: const InfoWindow(title: 'MAJID - delivery partner'),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Fetches a real road-following route from Google Directions API and
|
||||
/// draws it as a polyline.
|
||||
Future<void> _fetchRoute() async {
|
||||
try {
|
||||
final url = Uri.parse(
|
||||
'https://maps.googleapis.com/maps/api/directions/json'
|
||||
'?origin=${_pickup.latitude},${_pickup.longitude}'
|
||||
'&destination=${_drop.latitude},${_drop.longitude}'
|
||||
'&mode=driving'
|
||||
'&key=$kGoogleApiKey',
|
||||
);
|
||||
|
||||
final response = await http.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data['status'] == 'OK') {
|
||||
final points = data['routes'][0]['overview_polyline']['points'];
|
||||
_routePoints = _decodePolyline(points);
|
||||
} else {
|
||||
_routePoints = _fallbackRoute();
|
||||
}
|
||||
} else {
|
||||
_routePoints = _fallbackRoute();
|
||||
}
|
||||
} catch (_) {
|
||||
_routePoints = _fallbackRoute();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_polylines
|
||||
..clear()
|
||||
..add(
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: _routePoints,
|
||||
color: const Color(0xFF662582),
|
||||
width: 5,
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
jointType: JointType.round,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
_startRiderAnimation();
|
||||
}
|
||||
|
||||
/// Simple zig-zag fallback so the UI still shows a route without a live key.
|
||||
List<LatLng> _fallbackRoute() {
|
||||
return [
|
||||
_pickup,
|
||||
LatLng(_pickup.latitude - 0.0025, _pickup.longitude - 0.0015),
|
||||
LatLng(_pickup.latitude - 0.0035, _pickup.longitude - 0.0060),
|
||||
LatLng(_pickup.latitude - 0.0060, _pickup.longitude - 0.0075),
|
||||
_drop,
|
||||
];
|
||||
}
|
||||
|
||||
List<LatLng> _decodePolyline(String encoded) {
|
||||
List<LatLng> points = [];
|
||||
int index = 0, len = encoded.length;
|
||||
int lat = 0, lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
int b, shift = 0, result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlat = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlng = (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
|
||||
lng += dlng;
|
||||
|
||||
points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/// Moves the rider marker along the route to simulate live tracking.
|
||||
void _startRiderAnimation() {
|
||||
if (_routePoints.isEmpty) return;
|
||||
_riderTimer?.cancel();
|
||||
_riderTimer = Timer.periodic(const Duration(seconds: 2), (timer) {
|
||||
if (_routeIndex >= _routePoints.length - 1) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
_routeIndex++;
|
||||
_riderPosition = _routePoints[_routeIndex];
|
||||
_setupMarkers();
|
||||
setState(() {});
|
||||
_mapController?.animateCamera(CameraUpdate.newLatLng(_riderPosition));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _callPartner() async {
|
||||
final uri = Uri(scheme: 'tel', path: '+910000000000');
|
||||
if (await canLaunchUrl(uri)) await launchUrl(uri);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: SizedBox(),
|
||||
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: 'Out for delivery',
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
ReusableTextWidget(
|
||||
text: '11:38 AM • 1 items',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: const Color(0xFF662582),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
// ---------------- MAP ----------------
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: _pickup,
|
||||
zoom: 14.5,
|
||||
),
|
||||
markers: _markers,
|
||||
polylines: _polylines,
|
||||
myLocationButtonEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
compassEnabled: false,
|
||||
mapToolbarEnabled: false,
|
||||
buildingsEnabled: false,
|
||||
indoorViewEnabled: false,
|
||||
trafficEnabled: false,
|
||||
style: kSwiggyLikeMapStyle,
|
||||
onMapCreated: (controller) => _mapController = controller,
|
||||
),
|
||||
),
|
||||
|
||||
// -------------- TOP BAR --------------
|
||||
|
||||
|
||||
// SafeArea(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// _circleIconButton(Icons.arrow_back, () {
|
||||
// Navigator.maybePop(context);
|
||||
// }),
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// children: [
|
||||
// ReusableTextWidget(
|
||||
// text: 'Out for delivery',
|
||||
// fontSize: 17,
|
||||
// fontWeight: FontWeight.w700,
|
||||
// color: Colors.grey.shade900,
|
||||
// ),
|
||||
// const SizedBox(height: 2),
|
||||
// ReusableTextWidget(
|
||||
// text: '11:38 AM • 1 items',
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// color: Colors.grey.shade600,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(width: 40),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
// -------------- BOTTOM SHEET --------------
|
||||
DraggableScrollableSheet(
|
||||
initialChildSize: 0.42,
|
||||
minChildSize: 0.42,
|
||||
maxChildSize: 0.9,
|
||||
builder: (context, scrollController) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
// Out for delivery row
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: 'Out for delivery',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ReusableTextWidget(
|
||||
text: 'MAJID is on the way to deliver your order',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF7A1F5C),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: '$_etaMinutes',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
ReusableTextWidget(
|
||||
text: 'mins',
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Delivery partner info card
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.03),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
backgroundImage: const NetworkImage(
|
||||
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSmbomkQ5Aqtd848F_UVR86eY1wrq5tuNJRjmRXAGgXsqJtlL5H6EF8Efws&s=10'),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade500,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: _partnerName,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.star_rounded,
|
||||
size: 16, color: Colors.amber.shade700),
|
||||
const SizedBox(width: 4),
|
||||
ReusableTextWidget(
|
||||
text: '$_partnerRating',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ReusableTextWidget(
|
||||
text: '• $_partnerOrders orders',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: _callPartner,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFFFCE4EC),
|
||||
),
|
||||
child: const Icon(Icons.call,
|
||||
color: Colors.redAccent, size: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Action buttons row
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
|
||||
|
||||
Container(height: 8, color: const Color(0xFFF5F5F5)),
|
||||
|
||||
// Product / order items card
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.receipt_long, size: 20, color: Colors.grey.shade700),
|
||||
const SizedBox(width: 8),
|
||||
ReusableTextWidget(
|
||||
text: 'Order ID: # 123456789',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Items list
|
||||
_buildBillItem('Chicken Biryani', 1, 250),
|
||||
const SizedBox(height: 10),
|
||||
_buildBillItem('Mutton Biryani', 1, 350),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Price breakdown
|
||||
_buildPriceRow('Subtotal', 600),
|
||||
const SizedBox(height: 6),
|
||||
_buildPriceRow('Delivery Fee', 40),
|
||||
const SizedBox(height: 6),
|
||||
_buildPriceRow('Tax (5%)', 30),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Total
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: 'Total',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.grey.shade900,
|
||||
),
|
||||
ReusableTextWidget(
|
||||
text: '₹670',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.grey.shade900,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 6),
|
||||
ReusableTextWidget(
|
||||
text: label,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBillItem(String name, int qty, double price) {
|
||||
final total = qty * price;
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ReusableTextWidget(
|
||||
text: '${qty}x $name',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
ReusableTextWidget(
|
||||
text: '₹${price.toStringAsFixed(0)}',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ReusableTextWidget(
|
||||
text: '₹${total.toStringAsFixed(0)}',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceRow(String label, double amount) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ReusableTextWidget(
|
||||
text: label,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
ReusableTextWidget(
|
||||
text: '₹${amount.toStringAsFixed(0)}',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _circleIconButton(IconData icon, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 6),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: Colors.black87, size: 20),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user