Files
tripz-llc/apps/driver_new/lib/features/trip/view/duty_page.dart
T
2026-08-05 14:12:41 +03:00

221 lines
7.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/design/tokens.dart';
import '../../../core/design/tripz_colors.dart';
import '../../../core/di.dart';
import '../../../core/l10n/l10n.dart';
import '../../../core/ui/status_banner.dart';
import '../../../core/ui/app_drawer.dart';
import '../cubit/duty_cubit.dart';
import '../cubit/duty_state.dart';
import '../data/models/geo_point.dart';
import '../data/models/route_info.dart';
import 'widgets/active_duty_sheet.dart';
import 'widgets/duty_toggle.dart';
import 'widgets/offer_sheet.dart';
import 'widgets/ride_map.dart';
/// نصيب الورقة السفلية من ارتفاع الشاشة — مطابق لتطبيق الراكب.
const double _sheetFraction = 0.33;
/// ما تحتاجه الخريطة وحدها — تغيّرُ الرصيد أو الانشغال لا يعيد بناءها.
class _MapData {
const _MapData({this.me, this.pickup, this.dropoff, this.route, this.target});
final GeoPoint? me;
final GeoPoint? pickup;
final GeoPoint? dropoff;
final RouteInfo? route;
final GeoPoint? target;
@override
bool operator ==(Object other) =>
other is _MapData &&
other.me == me &&
other.pickup == pickup &&
other.dropoff == dropoff &&
other.route == route &&
other.target == target;
@override
int get hashCode => Object.hash(me, pickup, dropoff, route, target);
}
/// شاشة السائق: خريطة + مفتاح الاتصال أعلى + ورقة تتبدّل بالمرحلة.
class DutyPage extends StatefulWidget {
const DutyPage({super.key});
@override
State<DutyPage> createState() => _DutyPageState();
}
class _DutyPageState extends State<DutyPage> {
late final DutyCubit _cubit = sl<DutyCubit>()..init();
@override
void dispose() {
_cubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cubit,
child: Scaffold(
drawer: const AppDrawer(),
body: Stack(
children: [
// الخريطة معزولة عن بقية الحالة: نبضة موقع كل 25 متراً يجب ألّا
// تعيد بناء الورقة والمفتاح معها.
//
// **مرفوعة عن القاع بالثلث** كما في تطبيق الراكب — الورقة لها
// مساحتها ولا تغطّي الخريطة.
Positioned(
top: 0,
left: 0,
right: 0,
bottom: MediaQuery.sizeOf(context).height * _sheetFraction,
child: BlocSelector<DutyCubit, DutyState, _MapData>(
selector: (s) => _MapData(
me: s.myLocation,
pickup: s.trip?.origin,
dropoff: s.trip?.destination,
route: s.navRoute,
target: s.navTarget,
),
// السائق كان يرى **موقعه وحده**: بلا نقطة الراكب ولا الوجهة
// ولا خط يقوده. الخريطة الآن تعرض ما تعرضه خريطة الراكب:
// نقطتَي الرحلة والمسار الحيّ إلى الهدف الحالي.
builder: (context, d) => RideMap(
origin: d.pickup ?? d.me,
destination: d.dropoff,
driver: d.pickup == null ? null : d.me,
route: d.route,
// الكاميرا تتبع الهدف حين يوجد، وإلا فموقع السائق.
cameraTarget: d.target ?? d.me,
),
),
),
SafeArea(
child: Padding(
padding: Space.page,
child: Column(
children: [
Builder(
builder: (context) => Align(
alignment: AlignmentDirectional.topStart,
child: Material(
color: context.tripzColors.surfaceRaised,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: Scaffold.of(context).openDrawer,
child: const Padding(
padding: EdgeInsets.all(Space.sm),
child: Icon(Icons.menu_rounded, size: Sizes.icon),
),
),
),
),
),
const SizedBox(height: Space.xs),
BlocSelector<DutyCubit, DutyState, ({bool online, bool busy})>(
selector: (s) => (online: s.isOnline, busy: s.busy),
builder: (context, d) => DutyToggle(
isOnline: d.online,
busy: d.busy,
onToggle: _cubit.toggleOnline,
),
),
const SizedBox(height: Space.xs),
BlocSelector<DutyCubit, DutyState, bool>(
selector: (s) => s.creditBlocked,
builder: (context, blocked) => blocked
? StatusBanner(
message: context.l10n.creditBlocked,
tone: BannerTone.danger,
icon: Icons.account_balance_wallet_outlined,
)
: const SizedBox.shrink(),
),
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: BlocBuilder<DutyCubit, DutyState>(
builder: (context, state) => _Sheet(state: state),
),
),
],
),
),
);
}
}
class _Sheet extends StatelessWidget {
const _Sheet({required this.state});
final DutyState state;
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final child = switch (state.phase) {
DutyPhase.offline => const SizedBox.shrink(),
DutyPhase.waiting => Padding(
padding: const EdgeInsets.symmetric(vertical: Space.md),
child: Text(
state.error == DutyError.offerTaken
? l10n.driverOfferTaken
: l10n.driverNoOffers,
textAlign: TextAlign.center,
style: context.texts.bodyMedium?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
),
DutyPhase.offered => OfferSheet(offer: state.offer!),
DutyPhase.active =>
ActiveDutySheet(
trip: state.trip!,
busy: state.busy,
navRoute: state.navRoute,
),
};
if (state.phase == DutyPhase.offline) return const SizedBox.shrink();
return Container(
width: double.infinity,
// الحدّ الأدنى = المساحة المحجوزة تحت الخريطة، وإلا ظهر فراغ بينهما.
constraints: BoxConstraints(
minHeight: MediaQuery.sizeOf(context).height * _sheetFraction,
),
decoration: BoxDecoration(
color: context.colors.surface,
borderRadius: Radii.sheetTop,
),
child: SafeArea(
top: false,
child: Padding(
padding: Space.page,
child: AnimatedSize(
duration: Motion.inScreen,
curve: Motion.curve,
alignment: Alignment.bottomCenter,
child: child,
),
),
),
);
}
}