Update: 2026-07-12 05:40:28

This commit is contained in:
Hamza-Ayed
2026-07-12 05:40:28 +03:00
parent 1fa517acc9
commit 24fb56f08f
75 changed files with 5051 additions and 9 deletions
@@ -0,0 +1,147 @@
// transit_home_page.dart — الصفحة الرئيسية لتبويب "مواصلاتي"
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../controller/transit/transit_controller.dart';
import '../../controller/transit/transit_models.dart';
import '../widgets/my_scafold.dart';
import 'transit_org_browse_page.dart';
import 'transit_routes_page.dart';
class TransitHomePage extends StatelessWidget {
const TransitHomePage({super.key});
@override
Widget build(BuildContext context) {
Get.put(TransitController());
return GetBuilder<TransitController>(
builder: (c) => MyScafolld(
title: 'Mawasalati'.tr,
isleading: true,
body: [
Expanded(
child: RefreshIndicator(
onRefresh: c.fetchMyEnrollments,
child: c.isLoadingEnrollments
? const Center(child: CircularProgressIndicator())
: c.myEnrollments.isEmpty
? _emptyState(c)
: ListView(
padding: const EdgeInsets.all(16),
children: [
Text('My Memberships'.tr,
style: AppStyle.headTitle2.copyWith(fontSize: 18)),
const SizedBox(height: 12),
...c.myEnrollments.map((e) => _enrollmentCard(e, c)),
const SizedBox(height: 20),
_addOrgTile(),
],
),
),
),
],
),
);
}
Widget _emptyState(TransitController c) {
return ListView(
padding: const EdgeInsets.all(24),
children: [
const SizedBox(height: 60),
Icon(Icons.directions_bus_filled_outlined,
size: 72, color: AppColor.grayColor),
const SizedBox(height: 16),
Text(
'Not enrolled in any institution yet'.tr,
textAlign: TextAlign.center,
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Activate your membership in your university or institution to track your bus live'.tr,
textAlign: TextAlign.center,
style: AppStyle.subtitle.copyWith(color: AppColor.grayColor),
),
const SizedBox(height: 20),
_addOrgTile(),
],
);
}
Widget _addOrgTile() {
return Card(
color: AppColor.cardColor,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: AppColor.borderColor),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: CircleAvatar(
backgroundColor: AppColor.accentColor.withOpacity(0.15),
child: Icon(Icons.add, color: AppColor.accentColor),
),
title: Text('Join a new institution'.tr, style: AppStyle.title),
subtitle: Text('University, school, or hotel'.tr, style: AppStyle.subtitle),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => Get.to(() => const TransitOrgBrowsePage()),
),
);
}
Widget _enrollmentCard(TransitEnrollment e, TransitController c) {
Color statusColor;
String statusText;
switch (e.status) {
case 'active':
statusColor = AppColor.greenColor;
statusText = 'Active'.tr;
break;
case 'pending':
statusColor = AppColor.yellowColor;
statusText = 'Pending Review'.tr;
break;
default:
statusColor = AppColor.redColor;
statusText = 'Suspended'.tr;
}
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: AppColor.cardColor,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: AppColor.borderColor),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: CircleAvatar(
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
child: Icon(Icons.school_outlined, color: AppColor.primaryColor),
),
title: Text(e.orgName, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
subtitle: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: statusColor, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(statusText, style: AppStyle.subtitle.copyWith(color: statusColor)),
],
),
trailing: e.status == 'active' ? const Icon(Icons.arrow_forward_ios, size: 16) : null,
onTap: e.status == 'active'
? () => Get.to(() => TransitRoutesPage(orgId: e.orgId, orgName: e.orgName))
: null,
),
);
}
}
@@ -0,0 +1,146 @@
// transit_live_map_page.dart — تتبع الباص الحي على الخريطة
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 '../widgets/my_scafold.dart';
class TransitLiveMapPage extends StatefulWidget {
final int routeId;
final String routeName;
const TransitLiveMapPage({super.key, required this.routeId, required this.routeName});
@override
State<TransitLiveMapPage> createState() => _TransitLiveMapPageState();
}
class _TransitLiveMapPageState extends State<TransitLiveMapPage> {
IntaleqMapController? _mapController;
@override
void initState() {
super.initState();
Get.find<TransitController>().openLiveRoute(widget.routeId);
}
@override
void dispose() {
Get.find<TransitController>().closeLiveRoute();
super.dispose();
}
@override
Widget build(BuildContext context) {
return 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;
markers.add(Marker(
markerId: MarkerId('stop_${s['id']}'),
position: LatLng(lat, lng),
infoWindow: InfoWindow(title: s['name_ar']?.toString() ?? ''),
icon: InlqBitmap.defaultMarkerWithHue(
(s['is_major']?.toString() ?? '0') == '1' ? 30 : 200),
));
}
LatLng? busLatLng;
if (c.liveBusPosition != null) {
busLatLng = LatLng(c.liveBusPosition!.lat, c.liveBusPosition!.lng);
markers.add(Marker(
markerId: const MarkerId('bus'),
position: busLatLng,
rotation: c.liveBusPosition!.heading,
anchor: const Offset(0.5, 0.5),
icon: InlqBitmap.defaultMarkerWithHue(120),
));
if (_mapController != null) {
_mapController!.animateCamera(CameraUpdate.newLatLng(busLatLng));
}
}
final initialTarget = busLatLng ??
(markers.isNotEmpty ? markers.first.position : const LatLng(31.95, 35.93));
return MyScafolld(
title: widget.routeName,
isleading: true,
body: [
Expanded(
child: Stack(
children: [
IntaleqMap(
apiKey: Env.mapSaasKey,
initialCameraPosition: CameraPosition(target: initialTarget, zoom: 14),
markers: markers,
onMapCreated: (ctrl) => _mapController = ctrl,
),
if (c.isLoadingLiveTrip)
const Positioned.fill(
child: ColoredBox(
color: Colors.black12,
child: Center(child: CircularProgressIndicator()),
),
),
if (!c.isLoadingLiveTrip && c.liveError.isNotEmpty)
Positioned(
top: 16,
left: 16,
right: 16,
child: _statusBanner(c.liveError, AppColor.redColor),
),
if (!c.isLoadingLiveTrip &&
c.liveError.isEmpty &&
c.liveBusPosition == null)
Positioned(
top: 16,
left: 16,
right: 16,
child: _statusBanner(
'No bus running on this route right now'.tr, AppColor.yellowColor),
),
if (c.liveTripData?['trip']?['delay_minutes'] != null &&
(int.tryParse(c.liveTripData!['trip']['delay_minutes'].toString()) ?? 0) > 0)
Positioned(
bottom: 24,
left: 16,
right: 16,
child: _statusBanner(
'${'Bus is delayed'.tr} ${c.liveTripData!['trip']['delay_minutes']} ${'min'.tr}',
AppColor.yellowColor,
),
),
],
),
),
],
);
},
);
}
Widget _statusBanner(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppColor.cardColor,
borderRadius: BorderRadius.circular(12),
border: Border(right: BorderSide(color: color, width: 4)),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 6)],
),
child: Text(text, style: AppStyle.title.copyWith(color: color, fontWeight: FontWeight.bold)),
);
}
}
@@ -0,0 +1,145 @@
// transit_org_browse_page.dart — تصفح المؤسسات + تفعيل عضوية بالرقم الجامعي
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../controller/transit/transit_controller.dart';
import '../../controller/transit/transit_models.dart';
import '../widgets/elevated_btn.dart';
import '../widgets/my_scafold.dart';
class TransitOrgBrowsePage extends StatefulWidget {
const TransitOrgBrowsePage({super.key});
@override
State<TransitOrgBrowsePage> createState() => _TransitOrgBrowsePageState();
}
class _TransitOrgBrowsePageState extends State<TransitOrgBrowsePage> {
final _searchCtrl = TextEditingController();
@override
void initState() {
super.initState();
Get.find<TransitController>().browseOrgs();
}
@override
Widget build(BuildContext context) {
return GetBuilder<TransitController>(
builder: (c) => MyScafolld(
title: 'Choose your institution'.tr,
isleading: true,
body: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: TextField(
controller: _searchCtrl,
onSubmitted: (v) => c.browseOrgs(search: v),
decoration: InputDecoration(
hintText: 'Search for a university or institution...'.tr,
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: AppColor.cardColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColor.borderColor),
),
),
),
),
Expanded(
child: c.isLoadingOrgs
? const Center(child: CircularProgressIndicator())
: c.browsableOrgs.isEmpty
? Center(
child: Text('No results'.tr, style: AppStyle.title),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: c.browsableOrgs.length,
itemBuilder: (_, i) {
final org = c.browsableOrgs[i];
return Card(
margin: const EdgeInsets.only(bottom: 10),
color: AppColor.cardColor,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: AppColor.borderColor),
),
child: ListTile(
title: Text(org.nameAr, style: AppStyle.title),
subtitle: Text(org.city, style: AppStyle.subtitle),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => _showActivateSheet(context, c, org),
),
);
},
),
),
],
),
);
}
void _showActivateSheet(BuildContext context, TransitController c, TransitOrg org) {
final idCtrl = TextEditingController();
bool loading = false;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppColor.cardColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => StatefulBuilder(
builder: (ctx, setState) => Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: MediaQuery.of(ctx).viewInsets.bottom + 20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(org.nameAr, style: AppStyle.headTitle2.copyWith(fontSize: 18)),
const SizedBox(height: 6),
Text('Enter your student/employee ID to activate membership'.tr,
style: AppStyle.subtitle),
const SizedBox(height: 16),
TextField(
controller: idCtrl,
decoration: InputDecoration(
hintText: 'Student ID'.tr,
filled: true,
fillColor: AppColor.secondaryColor,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 16),
MyElevatedButton(
title: 'Activate'.tr,
isLoading: loading,
onPressed: () async {
if (idCtrl.text.trim().isEmpty) return;
setState(() => loading = true);
final ok = await c.activateEnrollment(org.id, idCtrl.text.trim());
setState(() => loading = false);
if (ok && ctx.mounted) {
Navigator.pop(ctx);
Get.back(); // العودة للصفحة الرئيسية لمواصلاتي
}
},
),
],
),
),
),
);
}
}
@@ -0,0 +1,74 @@
// transit_routes_page.dart — خطوط مؤسسة الراكب
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../controller/transit/transit_controller.dart';
import '../widgets/my_scafold.dart';
import 'transit_live_map_page.dart';
class TransitRoutesPage extends StatefulWidget {
final int orgId;
final String orgName;
const TransitRoutesPage({super.key, required this.orgId, required this.orgName});
@override
State<TransitRoutesPage> createState() => _TransitRoutesPageState();
}
class _TransitRoutesPageState extends State<TransitRoutesPage> {
@override
void initState() {
super.initState();
Get.find<TransitController>().openOrgRoutes(widget.orgId);
}
@override
Widget build(BuildContext context) {
return GetBuilder<TransitController>(
builder: (c) => MyScafolld(
title: widget.orgName,
isleading: true,
body: [
Expanded(
child: c.isLoadingRoutes
? const Center(child: CircularProgressIndicator())
: c.currentOrgRoutes.isEmpty
? Center(
child: Text('No active routes currently'.tr, style: AppStyle.title),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: c.currentOrgRoutes.length,
itemBuilder: (_, i) {
final r = c.currentOrgRoutes[i];
return Card(
margin: const EdgeInsets.only(bottom: 10),
color: AppColor.cardColor,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
side: BorderSide(color: AppColor.borderColor),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: AppColor.accentColor.withOpacity(0.15),
child: Icon(Icons.directions_bus, color: AppColor.accentColor),
),
title: Text(r.nameAr, style: AppStyle.title),
subtitle: Text('${r.stopCount} ${'Stops'.tr}', style: AppStyle.subtitle),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => Get.to(
() => TransitLiveMapPage(routeId: r.id, routeName: r.nameAr),
),
),
);
},
),
),
],
),
);
}
}