feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق
الباك إند: - تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً، وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً. - OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع. - N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر. N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)، ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird. فلاتر: - docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام). - السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب. - طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط. - الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3ab74a13a8
commit
de4bbd00ac
@@ -1,231 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import '../../../../controller/auth/captin/history_captain.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
class HistoryCaptain extends StatelessWidget {
|
||||
const HistoryCaptain({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(HistoryCaptainController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100], // A softer background color
|
||||
appBar: AppBar(
|
||||
title: Text('Ride History'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 1,
|
||||
),
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.historyData['message'].isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.history_toggle_off,
|
||||
size: 80, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No Rides Yet'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 动画: Wrap ListView with AnimationLimiter for staggered animations
|
||||
return AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: controller.historyData['message'].length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var trip = controller.historyData['message'][index];
|
||||
|
||||
// 动画: Apply animation to each list item
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _AnimatedHistoryCard(
|
||||
trip: trip,
|
||||
onTap: () {
|
||||
// Your original logic is preserved here
|
||||
if (trip['status'] != 'Cancel') {
|
||||
controller.getHistoryDetails(trip['order_id']);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'This Trip Was Cancelled'.tr,
|
||||
'This Trip Was Cancelled'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 动画: A new stateful widget to handle the tap animation
|
||||
class _AnimatedHistoryCard extends StatefulWidget {
|
||||
final Map<String, dynamic> trip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _AnimatedHistoryCard({required this.trip, required this.onTap});
|
||||
|
||||
@override
|
||||
__AnimatedHistoryCardState createState() => __AnimatedHistoryCardState();
|
||||
}
|
||||
|
||||
class __AnimatedHistoryCardState extends State<_AnimatedHistoryCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
_controller.reverse();
|
||||
widget.onTap();
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.receipt_long,
|
||||
color: Theme.of(context).primaryColor, size: 40),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${'OrderId'.tr}: ${widget.trip['order_id']}',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.trip['created_at'],
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildStatusChip(widget.trip['status']),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 A separate function for the status chip, slightly restyled for Material
|
||||
Widget _buildStatusChip(String status) {
|
||||
Color chipColor;
|
||||
Color textColor;
|
||||
String statusText = status;
|
||||
IconData iconData;
|
||||
|
||||
switch (status) {
|
||||
case 'Apply':
|
||||
chipColor = Colors.green.shade50;
|
||||
textColor = Colors.green.shade800;
|
||||
iconData = Icons.check_circle;
|
||||
break;
|
||||
case 'Refused':
|
||||
chipColor = Colors.red.shade50;
|
||||
textColor = Colors.red.shade800;
|
||||
iconData = Icons.cancel;
|
||||
break;
|
||||
case 'Cancel':
|
||||
chipColor = Colors.orange.shade50;
|
||||
textColor = Colors.orange.shade800;
|
||||
iconData = Icons.info;
|
||||
statusText = 'Cancelled';
|
||||
break;
|
||||
default:
|
||||
chipColor = Colors.grey.shade200;
|
||||
textColor = Colors.grey.shade800;
|
||||
iconData = Icons.hourglass_empty;
|
||||
}
|
||||
|
||||
return Chip(
|
||||
avatar: Icon(iconData, color: textColor, size: 16),
|
||||
label: Text(
|
||||
statusText.tr,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: chipColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
side: BorderSide.none,
|
||||
);
|
||||
}
|
||||
@@ -1,369 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import 'package:sefer_driver/controller/auth/captin/history_captain.dart';
|
||||
import 'package:sefer_driver/controller/functions/launch.dart';
|
||||
|
||||
class HistoryDetailsPage extends StatefulWidget {
|
||||
const HistoryDetailsPage({super.key});
|
||||
|
||||
@override
|
||||
State<HistoryDetailsPage> createState() => _HistoryDetailsPageState();
|
||||
}
|
||||
|
||||
class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
|
||||
// Get the controller instance
|
||||
final HistoryCaptainController controller =
|
||||
Get.find<HistoryCaptainController>();
|
||||
|
||||
// Helper method to safely parse LatLng from a string 'lat,lng'
|
||||
LatLng? _parseLatLng(String? latLngString) {
|
||||
if (latLngString == null) return null;
|
||||
final parts = latLngString.split(',');
|
||||
if (parts.length != 2) return null;
|
||||
final lat = double.tryParse(parts[0]);
|
||||
final lng = double.tryParse(parts[1]);
|
||||
if (lat == null || lng == null) return null;
|
||||
return LatLng(lat, lng);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text('Trip Details'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
),
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final res = controller.historyDetailsData['data'];
|
||||
if (res == null) {
|
||||
return Center(child: Text('Could not load trip details.'.tr));
|
||||
}
|
||||
|
||||
final startLocation = _parseLatLng(res['start_location']);
|
||||
final endLocation = _parseLatLng(res['end_location']);
|
||||
|
||||
// Create markers for the map
|
||||
final Set<Marker> markers = {};
|
||||
if (startLocation != null) {
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('start'),
|
||||
position: startLocation,
|
||||
infoWindow: InfoWindow(title: 'Start'.tr)));
|
||||
}
|
||||
if (endLocation != null) {
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('end'),
|
||||
position: endLocation,
|
||||
infoWindow: InfoWindow(title: 'End'.tr)));
|
||||
}
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: const Duration(milliseconds: 375),
|
||||
childAnimationBuilder: (widget) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(child: widget),
|
||||
),
|
||||
children: [
|
||||
// --- Map Card ---
|
||||
_buildMapCard(context, startLocation, endLocation, markers),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Trip Info Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.receipt_long,
|
||||
title: 'Trip Info'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Order ID'.tr,
|
||||
value: res['id']?.toString() ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Date'.tr,
|
||||
value: res['date']?.toString() ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Earnings Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.account_balance_wallet,
|
||||
title: 'Earnings & Distance'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Your Earnings'.tr,
|
||||
value: '${res['price_for_driver']}'),
|
||||
_InfoTile(
|
||||
label: 'Distance'.tr,
|
||||
value: '${res['distance']} KM'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Timeline Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.timeline,
|
||||
title: 'Trip Timeline'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Time to Passenger'.tr,
|
||||
value: res['DriverIsGoingToPassenger'] ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Trip Started'.tr,
|
||||
value: res['rideTimeStart'] ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Trip Finished'.tr,
|
||||
value: res['rideTimeFinish'] ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Passenger & Status Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.person,
|
||||
title: 'Passenger & Status'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Passenger Name'.tr,
|
||||
value:
|
||||
'${res['passengerName']} ${res['last_name']}'),
|
||||
_InfoTile(
|
||||
label: 'Status'.tr,
|
||||
value: res['status'] ?? 'N/A',
|
||||
isStatus: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMapCard(BuildContext context, LatLng? startLocation,
|
||||
LatLng? endLocation, Set<Marker> markers) {
|
||||
// A fallback position if locations are not available
|
||||
final initialCameraPosition = (startLocation != null)
|
||||
? CameraPosition(target: startLocation, zoom: 14)
|
||||
: const CameraPosition(
|
||||
target: LatLng(31.96, 35.92), zoom: 12); // Fallback to Amman
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
clipBehavior:
|
||||
Clip.antiAlias, // Ensures the map respects the border radius
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 250,
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: {
|
||||
if (startLocation != null && endLocation != null)
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: [startLocation, endLocation],
|
||||
color: Colors.deepPurple,
|
||||
width: 5,
|
||||
),
|
||||
},
|
||||
onMapCreated: (GoogleMapController mapController) {
|
||||
// Animate camera to fit the route
|
||||
if (startLocation != null && endLocation != null) {
|
||||
LatLngBounds bounds = LatLngBounds(
|
||||
southwest: LatLng(
|
||||
startLocation.latitude < endLocation.latitude
|
||||
? startLocation.latitude
|
||||
: endLocation.latitude,
|
||||
startLocation.longitude < endLocation.longitude
|
||||
? startLocation.longitude
|
||||
: endLocation.longitude,
|
||||
),
|
||||
northeast: LatLng(
|
||||
startLocation.latitude > endLocation.latitude
|
||||
? startLocation.latitude
|
||||
: endLocation.latitude,
|
||||
startLocation.longitude > endLocation.longitude
|
||||
? startLocation.longitude
|
||||
: endLocation.longitude,
|
||||
),
|
||||
);
|
||||
mapController.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 60.0));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: 'open_maps',
|
||||
onPressed: () {
|
||||
if (startLocation != null && endLocation != null) {
|
||||
String mapUrl =
|
||||
'https://www.google.com/maps/dir/${startLocation.latitude},${startLocation.longitude}/${endLocation.latitude},${endLocation.longitude}/';
|
||||
showInBrowser(mapUrl);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.directions),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A reusable widget for the main detail cards
|
||||
class _DetailCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
const _DetailCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: Theme.of(context).primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A reusable widget for a label-value pair inside a card
|
||||
class _InfoTile extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool isStatus;
|
||||
|
||||
const _InfoTile({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.isStatus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Colors.grey[700])),
|
||||
if (isStatus)
|
||||
_buildStatusChip(value)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reusing the status chip from the previous page for consistency
|
||||
Widget _buildStatusChip(String status) {
|
||||
Color chipColor;
|
||||
Color textColor;
|
||||
IconData iconData;
|
||||
|
||||
switch (status.toLowerCase()) {
|
||||
case 'apply':
|
||||
case 'completed': // Assuming 'Apply' means completed
|
||||
chipColor = Colors.green.shade50;
|
||||
textColor = Colors.green.shade800;
|
||||
iconData = Icons.check_circle;
|
||||
status = 'Completed';
|
||||
break;
|
||||
case 'refused':
|
||||
chipColor = Colors.red.shade50;
|
||||
textColor = Colors.red.shade800;
|
||||
iconData = Icons.cancel;
|
||||
break;
|
||||
case 'cancel':
|
||||
chipColor = Colors.orange.shade50;
|
||||
textColor = Colors.orange.shade800;
|
||||
iconData = Icons.info;
|
||||
status = 'Cancelled';
|
||||
break;
|
||||
default:
|
||||
chipColor = Colors.grey.shade200;
|
||||
textColor = Colors.grey.shade800;
|
||||
iconData = Icons.hourglass_empty;
|
||||
}
|
||||
|
||||
return Chip(
|
||||
avatar: Icon(iconData, color: textColor, size: 16),
|
||||
label: Text(
|
||||
status.tr,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: chipColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
side: BorderSide.none,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user