Files
intaleq/intaleq_rider/lib/views/transit/transit_map_page.dart
T
Hamza-AyedandClaude Opus 5 0aab85c732 refactor: إعادة تسمية التطبيقات الأربعة siro_* → intaleq_*
المجلدات وأسماء حزم dart واستيراداتها:
  siro_rider → intaleq_rider    siro_driver  → intaleq_driver
  siro_admin → intaleq_admin    siro_service → intaleq_service

شمل ذلك `name:` في كل pubspec وكل `package:siro_*` في الاستيرادات
(115 ملفاً في rider وحده)، وإشارات أسماء المجلدات في docs/ وتعليق في
backend/Admin/notifications/broadcast.php. لم تبقَ إشارة واحدة (تحقّقت).

+ ضُمّت حزم get و get_storage داخل intaleq_driver/packages/ وحُوّلت
مساراتها الثلاثة من `../../Intaleq/packages/*` إلى `./packages/*`. كانت
تُحل عرضاً إلى مجلد App/Intaleq القديم المجاور — تبعية خارج المستودع
تنكسر بأول نقل. لم يبقَ في أي تطبيق تبعية مسار خارج الشجرة.

⚠️ لم تُمسّ بعد: هويات المتاجر (applicationId · PRODUCT_BUNDLE_IDENTIFIER ·
shorebird app_id) ولا الألوان ولا النصوص المرئية ولا النطاقات — كل منها
كوميت منفصل، والهويات تحتاج قرار المالك.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:10:58 +03:00

528 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// transit_map_page.dart — خريطة الباص الحي (مواصلاتي)
// خريطة مستقلة داخل مجلد transit — لا تمس home_captain ولا passenger map
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../env/env.dart';
import '../../controller/transit/transit_controller.dart';
import 'transit_qr_code_page.dart';
import '../widgets/elevated_btn.dart';
// ── ألوان الصفحة ──────────────────────────────────────────────────────────────
Color get _bg =>
Get.isDarkMode ? const Color(0xFF0A0F1E) : const Color(0xFFF0F4FF);
Color get _card =>
Get.isDarkMode ? const Color(0xFF151C2E) : Colors.white;
Color get _text =>
Get.isDarkMode ? const Color(0xFFEEF2FF) : const Color(0xFF1E293B);
Color get _sub =>
Get.isDarkMode ? const Color(0xFF94A3B8) : const Color(0xFF64748B);
const Color _accent = Color(0xFF2563EB);
class TransitMapPage extends StatefulWidget {
final int routeId;
final String routeName;
const TransitMapPage({super.key, required this.routeId, required this.routeName});
@override
State<TransitMapPage> createState() => _TransitMapPageState();
}
class _TransitMapPageState extends State<TransitMapPage> {
IntaleqMapController? _mapCtrl;
InlqBitmap _busIcon = InlqBitmap.defaultMarkerWithHue(120);
bool _busIconLoaded = false;
bool _cameraLocked = true;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
// تحميل أيقونة الباص من الأصول (bus.png: 499×499، size يضبط الحجم المرئي ~80dp)
try {
_busIcon = InlqBitmap.fromAsset(
'assets/images/bus.png',
size: 0.40,
);
_busIconLoaded = true;
} catch (_) {
_busIcon = InlqBitmap.defaultMarkerWithHue(120);
}
// تحميل بيانات الرحلة + اشتراك السوكيت
await Get.find<TransitController>().openLiveRoute(widget.routeId);
// اشتراك FCM لتنبيهات الخط
Get.find<TransitController>().subscribeToRouteTopic(widget.routeId);
});
}
@override
void dispose() {
Get.find<TransitController>().closeLiveRoute();
Get.find<TransitController>().unsubscribeFromRouteTopic(widget.routeId);
super.dispose();
}
@override
Widget build(BuildContext context) {
final bottomPad = MediaQuery.of(context).padding.bottom;
return Scaffold(
backgroundColor: _bg,
body: GetBuilder<TransitController>(
builder: (c) {
// ── بناء ماركرات المحطات والباص ─────────────────────────
final stops = (c.liveTripData?['stops'] is List)
? List<Map>.from(c.liveTripData!['stops'])
: <Map>[];
final markers = <Marker>{};
// ماركر المحطات
for (final s in stops) {
final lat = double.tryParse(s['latitude']?.toString() ?? '') ?? 0;
final lng = double.tryParse(s['longitude']?.toString() ?? '') ?? 0;
if (lat == 0 && lng == 0) continue;
final isMajor = (s['is_major']?.toString() ?? '0') == '1';
markers.add(Marker(
markerId: MarkerId('stop_${s['id']}'),
position: LatLng(lat, lng),
infoWindow: InfoWindow(title: s['name_ar']?.toString() ?? ''),
icon: InlqBitmap.defaultMarkerWithHue(isMajor ? 30 : 200),
zIndex: 1,
));
}
// ماركر الباص — أيقونة مخصصة 80×80 بدون خلفية
LatLng? busPos;
if (c.liveBusPosition != null) {
busPos = LatLng(c.liveBusPosition!.lat, c.liveBusPosition!.lng);
markers.add(Marker(
markerId: const MarkerId('transit_bus'),
position: busPos,
rotation: c.liveBusPosition!.heading,
anchor: const Offset(0.5, 0.5),
flat: true,
zIndex: 3,
icon: _busIconLoaded ? _busIcon : InlqBitmap.defaultMarkerWithHue(120),
));
// تتبع الكاميرا للباص
if (_cameraLocked && _mapCtrl != null) {
_mapCtrl!.animateCamera(CameraUpdate.newLatLng(busPos));
}
}
// ── polyline الخط ──────────────────────────────────────
final polylines = <Polyline>{};
final encodedPolyline =
c.liveTripData?['trip']?['route_polyline']?.toString() ?? '';
if (encodedPolyline.isNotEmpty) {
final coords = PolylineUtils.decode(encodedPolyline);
if (coords.isNotEmpty) {
polylines.add(Polyline(
polylineId: const PolylineId('transit_route'),
points: coords,
color: _accent.withOpacity(0.7),
width: 4,
));
}
}
final initialTarget = busPos ??
(markers.isNotEmpty
? markers.first.position
: const LatLng(31.95, 35.93));
// ── معلومات البطاقة السفلية ────────────────────────────
final trip = c.liveTripData?['trip'];
final driverName = trip?['driver_name']?.toString() ?? '';
final departure = trip?['departure_time']?.toString() ?? '';
final delayMin =
int.tryParse(trip?['delay_minutes']?.toString() ?? '0') ?? 0;
final currentStopSeq = c.liveBusPosition?.currentStopSeq;
final currentStopName = currentStopSeq != null && stops.isNotEmpty
? stops
.where((s) =>
int.tryParse(s['sequence']?.toString() ?? '') ==
currentStopSeq)
.map((s) => s['name_ar']?.toString() ?? '')
.firstOrNull
: null;
return Stack(
children: [
// ── 1. الخريطة ─────────────────────────────────────
Listener(
onPointerDown: (_) {
if (_cameraLocked) setState(() => _cameraLocked = false);
},
child: IntaleqMap(
apiKey: Env.mapSaasKey,
initialCameraPosition:
CameraPosition(target: initialTarget, zoom: 14.5),
markers: markers,
polylines: polylines,
onMapCreated: (ctrl) => _mapCtrl = ctrl,
mapType: Get.isDarkMode
? IntaleqMapType.normal
: IntaleqMapType.light,
myLocationEnabled: true,
),
),
// ── 2. شريط العنوان ────────────────────────────────
Positioned(
top: MediaQuery.of(context).padding.top + 8,
left: 12,
right: 12,
child: Row(
children: [
// زر الرجوع
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: _card,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 10,
offset: const Offset(0, 3))
],
),
child: Icon(Icons.arrow_back_ios_rounded,
color: _text, size: 18),
),
),
const SizedBox(width: 10),
// اسم الخط
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _card,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 10,
offset: const Offset(0, 3))
],
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: c.liveBusPosition != null
? Colors.greenAccent.shade400
: Colors.grey,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.routeName,
style: AppStyle.title.copyWith(
fontSize: 14,
color: _text,
fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
const SizedBox(width: 10),
// زر إعادة تثبيت الكاميرا
if (!_cameraLocked)
GestureDetector(
onTap: () {
setState(() => _cameraLocked = true);
if (busPos != null && _mapCtrl != null) {
_mapCtrl!.animateCamera(
CameraUpdate.newLatLngZoom(busPos!, 15));
}
},
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: _accent,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: _accent.withValues(alpha: 0.4),
blurRadius: 10,
offset: const Offset(0, 3))
],
),
child: const Icon(Icons.my_location_rounded,
color: Colors.white, size: 20),
),
),
],
),
),
// ── 3. بانر التأخير ────────────────────────────────
if (delayMin > 0)
Positioned(
top: MediaQuery.of(context).padding.top + 66,
left: 12,
right: 12,
child: _statusBanner(
'🕐 ${'Bus is delayed'.tr} $delayMin ${'min'.tr}',
const Color(0xFFF59E0B),
),
),
// ── 4. حالة التحميل أو الخطأ ─────────────────────
if (c.isLoadingLiveTrip)
Positioned.fill(
child: Container(
color: Colors.black26,
child: const Center(child: CircularProgressIndicator()),
),
),
if (!c.isLoadingLiveTrip && c.liveError.isNotEmpty)
Positioned(
top: MediaQuery.of(context).padding.top + 70,
left: 16,
right: 16,
child: _statusBanner(c.liveError, Colors.redAccent),
),
if (!c.isLoadingLiveTrip &&
c.liveError.isEmpty &&
c.liveBusPosition == null &&
!c.isLoadingLiveTrip)
Positioned(
top: MediaQuery.of(context).padding.top + 70,
left: 16,
right: 16,
child: _statusBanner(
'No bus running on this route right now'.tr,
const Color(0xFFF59E0B),
),
),
// ── 5. بطاقة المعلومات السفلية ─────────────────────
if (!c.isLoadingLiveTrip && trip != null)
Positioned(
bottom: bottomPad + 12,
left: 12,
right: 12,
child: _InfoCard(
driverName: driverName,
departure: departure,
currentStopName: currentStopName,
totalStops: stops.length,
currentStopSeq: currentStopSeq,
onMissedBus: () {
// CTA: فاتك الباص؟ اطلب رحلة
Get.back();
Get.toNamed('/home');
},
),
),
],
);
},
),
);
}
Widget _statusBanner(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _card,
borderRadius: BorderRadius.circular(14),
border: Border(right: BorderSide(color: color, width: 4)),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8)
],
),
child: Text(text,
style: AppStyle.subtitle.copyWith(
color: color, fontWeight: FontWeight.bold, fontSize: 13)),
);
}
}
// ── بطاقة معلومات الرحلة ────────────────────────────────────────────────────
class _InfoCard extends StatelessWidget {
final String driverName;
final String departure;
final String? currentStopName;
final int totalStops;
final int? currentStopSeq;
final VoidCallback onMissedBus;
const _InfoCard({
required this.driverName,
required this.departure,
required this.currentStopName,
required this.totalStops,
required this.currentStopSeq,
required this.onMissedBus,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: _card,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 20,
offset: const Offset(0, -4))
],
),
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── صف المعلومات ─────────────────────────────
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: _accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.directions_bus_rounded,
color: _accent, size: 26),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (driverName.isNotEmpty)
Text(driverName,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: _text)),
const SizedBox(height: 2),
Row(
children: [
if (departure.isNotEmpty) ...[
Icon(Icons.schedule_rounded, size: 13, color: _sub),
const SizedBox(width: 3),
Text(departure,
style: TextStyle(fontSize: 12, color: _sub)),
const SizedBox(width: 12),
],
if (currentStopSeq != null) ...[
Icon(Icons.location_on_rounded,
size: 13, color: Colors.greenAccent.shade400),
const SizedBox(width: 3),
Text(
currentStopName ??
'${'Stop'.tr} $currentStopSeq',
style:
TextStyle(fontSize: 12, color: _sub),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
],
),
),
// عداد المحطات
if (totalStops > 0 && currentStopSeq != null)
Column(
children: [
Text(
'$currentStopSeq/$totalStops',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: _accent),
),
Text('Stops'.tr,
style: TextStyle(fontSize: 10, color: _sub)),
],
),
],
),
// ── شريط التقدم بين المحطات ─────────────────
if (totalStops > 0 && currentStopSeq != null) ...[
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: currentStopSeq! / totalStops,
backgroundColor: _accent.withValues(alpha: 0.12),
valueColor: const AlwaysStoppedAnimation<Color>(_accent),
minHeight: 6,
),
),
],
const SizedBox(height: 12),
// ── زر "بطاقة الصعود" (Boarding Pass) ─────────────────
SizedBox(
width: double.infinity,
height: 55,
child: MyElevatedButton(
onPressed: () {
Get.to(() => const TransitQRCodePage());
},
kolor: _accent,
title: 'بطاقة الصعود (Boarding Pass)'.tr,
),
),
const SizedBox(height: 8),
// ── زر "فاتك الباص؟" ─────────────────────────
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: onMissedBus,
style: OutlinedButton.styleFrom(
side: BorderSide(color: _accent.withValues(alpha: 0.5)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 10),
),
icon: const Icon(Icons.local_taxi_rounded, size: 18),
label: Text(
'Missed the bus? Book a ride now'.tr,
style: const TextStyle(fontSize: 13),
),
),
),
],
),
);
}
}