207 lines
7.1 KiB
Dart
207 lines
7.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import '../../controller/scheduled/scheduled_rides_controller.dart';
|
|
|
|
/// شاشة حجوزات الراكب المسبقة.
|
|
///
|
|
/// تعرض القادم فقط: الحجز الفائت لا يفيد، والراكب يفتح الشاشة ليرى ما
|
|
/// ينتظره لا ليتصفّح أرشيفه.
|
|
class ScheduledRidesPage extends StatelessWidget {
|
|
const ScheduledRidesPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = Get.put(ScheduledRidesController());
|
|
|
|
return Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Scaffold(
|
|
appBar: AppBar(title: Text('رحلاتي المحجوزة'.tr)),
|
|
body: Obx(() {
|
|
if (c.isLoading.value && c.bookings.isEmpty) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (c.bookings.isEmpty) {
|
|
return _Empty(maxDays: ScheduledRidesController.maxDaysAhead);
|
|
}
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: c.fetch,
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: c.bookings.length,
|
|
itemBuilder: (_, i) => _BookingCard(b: c.bookings[i], c: c),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Empty extends StatelessWidget {
|
|
const _Empty({required this.maxDays});
|
|
final int maxDays;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
return ListView(
|
|
padding: const EdgeInsets.all(32),
|
|
children: [
|
|
const SizedBox(height: 80),
|
|
Icon(Icons.event_available_outlined,
|
|
size: 64, color: cs.primary.withOpacity(0.4)),
|
|
const SizedBox(height: 20),
|
|
Text('لا حجوزات قادمة'.tr,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'احجز رحلتك للمطار أو موعدك الطبي أو دوامك الصباحي'
|
|
' حتى $maxDays يومين مقدماً، ونرسل لك سائقاً في وقته.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 13, height: 1.6, color: cs.onSurfaceVariant),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BookingCard extends StatelessWidget {
|
|
const _BookingCard({required this.b, required this.c});
|
|
final Map<String, dynamic> b;
|
|
final ScheduledRidesController c;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
final when = DateTime.tryParse('${b['scheduled_at']}');
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.schedule, size: 18, color: Color(0xFF6A5ACD)),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
when == null ? '${b['scheduled_at']}' : _when(when),
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold, fontSize: 15),
|
|
),
|
|
const Spacer(),
|
|
if (when != null) _Countdown(when: when),
|
|
],
|
|
),
|
|
const Divider(height: 20),
|
|
_row(context, '🟢', b['start_name'] ?? 'موقع الانطلاق'),
|
|
const SizedBox(height: 6),
|
|
_row(context, '🔴', b['end_name'] ?? 'الوجهة'),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Text('${b['car_type'] ?? ''}',
|
|
style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)),
|
|
const Spacer(),
|
|
// نصرّح بأن السعر تقديري: التسعير النهائي لحظة إنشاء
|
|
// الرحلة، فرقم اليوم قد لا يطابق رقم الغد.
|
|
Text(
|
|
'تقديري: ${b['estimated_price'] ?? '—'}',
|
|
style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: TextButton.icon(
|
|
onPressed: () => _confirmCancel(context),
|
|
icon: Icon(Icons.close, size: 16, color: cs.error),
|
|
label: Text('إلغاء الحجز'.tr,
|
|
style: TextStyle(color: cs.error, fontSize: 13)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _confirmCancel(BuildContext context) {
|
|
Get.defaultDialog(
|
|
title: 'إلغاء الحجز'.tr,
|
|
// بلا رسم: لا سائق قُبِل ولا أحد تحرّك. نقولها صراحةً ليطمئن الراكب.
|
|
middleText: 'سيُلغى الحجز بلا أي رسوم — لم يُسند سائق بعد.'.tr,
|
|
textConfirm: 'تأكيد'.tr,
|
|
textCancel: 'تراجع'.tr,
|
|
onConfirm: () {
|
|
Get.back();
|
|
final id = int.tryParse('${b['id']}');
|
|
if (id != null) c.cancel(id);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _row(BuildContext context, String dot, dynamic text) => Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(dot, style: const TextStyle(fontSize: 12)),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text('$text',
|
|
style: const TextStyle(fontSize: 13, height: 1.4)),
|
|
),
|
|
],
|
|
);
|
|
|
|
String _when(DateTime d) {
|
|
final now = DateTime.now();
|
|
final isToday =
|
|
d.year == now.year && d.month == now.month && d.day == now.day;
|
|
final tomorrow = now.add(const Duration(days: 1));
|
|
final isTomorrow = d.year == tomorrow.year &&
|
|
d.month == tomorrow.month &&
|
|
d.day == tomorrow.day;
|
|
|
|
final hh = d.hour.toString().padLeft(2, '0');
|
|
final mm = d.minute.toString().padLeft(2, '0');
|
|
|
|
if (isToday) return 'اليوم $hh:$mm';
|
|
if (isTomorrow) return 'غداً $hh:$mm';
|
|
return '${d.day}/${d.month} — $hh:$mm';
|
|
}
|
|
}
|
|
|
|
/// كم تبقّى للموعد. يجعل الحجز محسوساً بدل تاريخ جامد.
|
|
class _Countdown extends StatelessWidget {
|
|
const _Countdown({required this.when});
|
|
final DateTime when;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final diff = when.difference(DateTime.now());
|
|
if (diff.isNegative) return const SizedBox.shrink();
|
|
|
|
final label = diff.inHours >= 1
|
|
? 'بعد ${diff.inHours} ساعة'
|
|
: 'بعد ${diff.inMinutes} دقيقة';
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF6A5ACD).withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(label,
|
|
style: const TextStyle(fontSize: 11, color: Color(0xFF6A5ACD))),
|
|
);
|
|
}
|
|
}
|