import 'package:siro_admin/constant/theme.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; import 'package:latlong2/latlong.dart'; import '../../../controller/admin/live_analytics_controller.dart'; class LiveAnalyticsPage extends StatelessWidget { const LiveAnalyticsPage({super.key}); @override Widget build(BuildContext context) { final c = Get.put(LiveAnalyticsController()); final cs = Theme.of(context).colorScheme; return DefaultTabController( length: 4, child: Scaffold( backgroundColor: cs.surface, body: Column( children: [ Container( padding: const EdgeInsets.fromLTRB(16, 50, 16, 16), child: Row( children: [ GestureDetector( onTap: () => Get.back(), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(10), border: Border.all(color: cs.outline), ), child: Icon(Icons.arrow_back_ios_new_rounded, color: cs.onSurfaceVariant, size: 16), ), ), const SizedBox(width: 12), Text('لوحة البيانات التفاعلية', style: TextStyle(color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w700)), const Spacer(), IconButton( icon: const Icon(Icons.calendar_month_rounded, size: 22), onPressed: () => _pickDate(context, c), ), IconButton( icon: const Icon(Icons.refresh_rounded, size: 22), onPressed: () => c.fetchAll(), ), ], ), ), Container( color: cs.surface, child: TabBar( isScrollable: true, indicatorColor: cs.primary, labelColor: cs.primary, unselectedLabelColor: cs.onSurfaceVariant, labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13), tabs: const [ Tab(text: 'الخريطة التفاعلية', icon: Icon(Icons.map_rounded, size: 18)), Tab(text: 'السوق والتسعير', icon: Icon(Icons.trending_up_rounded, size: 18)), Tab(text: 'الأسطول والجودة', icon: Icon(Icons.local_taxi_rounded, size: 18)), Tab(text: 'النمو والاحتفاظ', icon: Icon(Icons.show_chart_rounded, size: 18)), ], ), ), Expanded( child: GetBuilder( builder: (ctrl) { if (ctrl.isLoading) { return Center( child: CircularProgressIndicator(color: cs.primary), ); } if (ctrl.allData.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.cloud_off_rounded, size: 48, color: cs.onSurfaceVariant), const SizedBox(height: 12), Text('لا توجد بيانات لتاريخ ${ctrl.selectedDate}', style: TextStyle(color: cs.onSurfaceVariant)), const SizedBox(height: 8), if (ctrl.availableDates.isNotEmpty) Text('آخر تاريخ متاح: ${ctrl.availableDates.first}', style: TextStyle( color: cs.primary, fontSize: 13)), ], ), ); } return TabBarView( children: [ _MapTab(ctrl: ctrl), _MarketTab(ctrl: ctrl), _FleetTab(ctrl: ctrl), _GrowthTab(ctrl: ctrl), ], ); }, ), ), ], ), ), ); } void _pickDate(BuildContext context, LiveAnalyticsController c) async { final cs = Theme.of(context).colorScheme; final picked = await showDatePicker( context: context, initialDate: DateTime.tryParse(c.selectedDate) ?? DateTime.now(), firstDate: DateTime(2024), lastDate: DateTime.now(), builder: (ctx, child) => Theme( data: ThemeData.dark().copyWith( colorScheme: ColorScheme.dark(primary: cs.primary), ), child: child!, ), ); if (picked != null) { c.changeDate( '${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}'); } } } // ═══════════════════════════════════════════════════════════════ // Shared Widgets // ═══════════════════════════════════════════════════════════════ class _StatCard extends StatelessWidget { final String label; final String value; final String? sub; final Color? subColor; final IconData? icon; const _StatCard({ required this.label, required this.value, this.sub, this.subColor, this.icon, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), border: Border.all(color: cs.outline), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (icon != null) ...[ Icon(icon, size: 16, color: cs.onSurfaceVariant), const SizedBox(width: 6), ], Expanded( child: Text(label, style: TextStyle( fontSize: 11, color: cs.onSurfaceVariant, fontWeight: FontWeight.w600), maxLines: 1, overflow: TextOverflow.ellipsis), ), ], ), const SizedBox(height: 4), FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerRight, child: Text(value, style: TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: cs.onSurface)), ), if (sub != null) ...[ const SizedBox(height: 2), Text(sub!, style: TextStyle( fontSize: 10, color: subColor ?? cs.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis), ], ], ), ); } } class _SectionTitle extends StatelessWidget { final String title; const _SectionTitle(this.title); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: 12, top: 8), child: Text(title, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: cs.onSurface)), ); } } class _ChartCard extends StatelessWidget { final String title; final Widget child; final double height; const _ChartCard({ required this.title, required this.child, this.height = 220, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), border: Border.all(color: cs.outline), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: cs.onSurfaceVariant)), const SizedBox(height: 16), SizedBox(height: height, child: child), ], ), ); } } String _fmt(dynamic n) { if (n == null) return '—'; if (n is num) { if (n >= 1000) return '${(n / 1000).toStringAsFixed(1)}k'; return n % 1 == 0 ? n.toInt().toString() : n.toStringAsFixed(1); } return n.toString(); } double _toDouble(dynamic v) { if (v is num) return v.toDouble(); if (v is String) return double.tryParse(v) ?? 0; return 0; } int _toInt(dynamic v) { if (v is int) return v; if (v is num) return v.toInt(); if (v is String) return int.tryParse(v) ?? 0; return 0; } // ═══════════════════════════════════════════════════════════════ // TAB 1: الخريطة التفاعلية // ═══════════════════════════════════════════════════════════════ class _MapTab extends StatefulWidget { final LiveAnalyticsController ctrl; const _MapTab({required this.ctrl}); @override State<_MapTab> createState() => _MapTabState(); } class _MapTabState extends State<_MapTab> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; bool showGap = true; bool showHeatmap = true; bool showSupply = false; bool showPricing = false; bool showZones = false; @override Widget build(BuildContext context) { super.build(context); final cs = Theme.of(context).colorScheme; final c = widget.ctrl; final rt = c.realtime; final revToday = _toDouble(rt['revenue_today']); final revYesterday = _toDouble(rt['revenue_yesterday']); final revChange = revYesterday > 0 ? ((revToday - revYesterday) / revYesterday * 100) : 0.0; return ListView( padding: const EdgeInsets.all(16), children: [ GridView.count( crossAxisCount: MediaQuery.of(context).size.width > 600 ? 5 : 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.6, children: [ _StatCard( label: 'رحلات نشطة', value: _fmt(rt['active_rides']), icon: Icons.directions_car_rounded, ), _StatCard( label: 'كباتن أونلاين', value: _fmt(rt['online_drivers']), icon: Icons.person_pin_circle_rounded, ), _StatCard( label: 'إيرادات اليوم', value: _fmt(revToday), sub: '${revChange >= 0 ? '+' : ''}${revChange.toStringAsFixed(1)}% عن الأمس', subColor: revChange >= 0 ? cs.success : cs.error, icon: Icons.attach_money_rounded, ), _StatCard( label: 'شكاوى مفتوحة', value: _fmt(rt['open_complaints']), icon: Icons.warning_amber_rounded, ), _StatCard( label: 'رخص تنتهي قريباً', value: _fmt(rt['expiring_licenses']), icon: Icons.badge_rounded, ), ], ), const SizedBox(height: 20), const _SectionTitle('خريطة فجوة العرض والطلب'), _buildLayerToggles(cs), const SizedBox(height: 8), _buildMap(c, cs), const SizedBox(height: 20), _buildHourlyChart(c, cs), _buildFunnelChart(c, cs), ], ); } Widget _buildLayerToggles(ColorScheme cs) { return Wrap( spacing: 8, runSpacing: 8, children: [ _LayerChip('فجوة العرض/الطلب', showGap, Colors.redAccent, (v) => setState(() => showGap = v)), _LayerChip('كثافة الطلب', showHeatmap, Colors.orange, (v) => setState(() => showHeatmap = v)), _LayerChip('مواقع الكباتن', showSupply, cs.success, (v) => setState(() => showSupply = v)), _LayerChip('أسعار المنافسين', showPricing, cs.tertiary, (v) => setState(() => showPricing = v)), _LayerChip('أعلى المناطق', showZones, cs.primary, (v) => setState(() => showZones = v)), ], ); } Widget _buildMap(LiveAnalyticsController c, ColorScheme cs) { final gapCells = (c.gap['cells'] as List?) ?? []; final heatmapData = c.heatmap['data']; final pricingGrids = c.pricing['grids']; final topZones = (c.zones['zones'] as List?) ?? []; List markers = []; if (showGap) { for (var cell in gapCells) { final lat = _toDouble(cell['lat']); final lng = _toDouble(cell['lng']); final gap = _toInt(cell['gap']); if (lat == 0 || lng == 0) continue; final intensity = (gap.abs() / 5).clamp(0.0, 1.0); markers.add(CircleMarker( point: LatLng(lat, lng), color: gap > 0 ? Colors.redAccent.withValues(alpha: 0.3 + intensity * 0.5) : cs.success.withValues(alpha: 0.4), borderStrokeWidth: 0, radius: 12 + intensity * 8, )); } } if (showHeatmap && heatmapData is Map) { for (var entry in heatmapData.entries) { if (entry.value is! List) continue; for (var pt in (entry.value as List).take(500)) { final lat = _toDouble(pt['lat']); final lng = _toDouble(pt['lng']); if (lat == 0 || lng == 0) continue; markers.add(CircleMarker( point: LatLng(lat, lng), color: Colors.orange.withValues(alpha: 0.35), borderStrokeWidth: 0, radius: 5, )); } } } if (showSupply) { for (var cell in gapCells) { final supply = _toInt(cell['supply']); if (supply <= 0) continue; final lat = _toDouble(cell['lat']); final lng = _toDouble(cell['lng']); if (lat == 0 || lng == 0) continue; markers.add(CircleMarker( point: LatLng(lat, lng), color: cs.success.withValues(alpha: 0.5), borderStrokeWidth: 1, borderColor: Colors.white.withValues(alpha: 0.4), radius: 6 + supply.clamp(0, 10).toDouble(), )); } } if (showPricing && pricingGrids is Map) { for (var entry in pricingGrids.entries) { final key = entry.key.toString(); if (key.contains('FALLBACK')) continue; final parts = key.split('_'); if (parts.length < 3) continue; final lat = double.tryParse(parts[1]) ?? 0; final lng = double.tryParse(parts[2]) ?? 0; if (lat == 0 || lng == 0) continue; markers.add(CircleMarker( point: LatLng(lat, lng), color: cs.tertiary.withValues(alpha: 0.5), borderStrokeWidth: 1, borderColor: Colors.white.withValues(alpha: 0.3), radius: 8, )); } } if (showZones) { for (var z in topZones) { final lat = _toDouble(z['lat']); final lng = _toDouble(z['lng']); final rides = _toInt(z['rides']); if (lat == 0 || lng == 0) continue; markers.add(CircleMarker( point: LatLng(lat, lng), color: cs.primary.withValues(alpha: 0.5), borderStrokeWidth: 2, borderColor: Colors.white.withValues(alpha: 0.5), radius: 10 + (rides / 10).clamp(0, 15).toDouble(), )); } } LatLng center = const LatLng(31.95, 35.93); if (gapCells.isNotEmpty) { center = LatLng( _toDouble(gapCells.first['lat']), _toDouble(gapCells.first['lng'])); } return ClipRRect( borderRadius: BorderRadius.circular(12), child: SizedBox( height: 400, child: FlutterMap( options: MapOptions(initialCenter: center, initialZoom: 11), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', subdomains: const ['a', 'b', 'c', 'd'], userAgentPackageName: 'com.siromove.admin', ), CircleLayer(circles: markers), ], ), ), ); } Widget _buildHourlyChart(LiveAnalyticsController c, ColorScheme cs) { final hours = (c.hourly['hours'] as List?) ?? []; if (hours.isEmpty) return const SizedBox.shrink(); return _ChartCard( title: 'توزيع الرحلات حسب الساعة (آخر 7 أيام)', child: BarChart( BarChartData( barGroups: hours.asMap().entries.map((e) { return BarChartGroupData(x: e.key, barRods: [ BarChartRodData( toY: _toDouble(e.value['rides']), color: cs.primary.withValues(alpha: 0.7), width: 10, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ]); }).toList(), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= hours.length) return const SizedBox(); return Text('${hours[i]['hour']}', style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), ), ), ); } Widget _buildFunnelChart(LiveAnalyticsController c, ColorScheme cs) { final statuses = (c.funnel['statuses'] as List?) ?? []; if (statuses.isEmpty) return const SizedBox.shrink(); final colors = { 'Finished': cs.success, 'wait': cs.tertiary, 'started': cs.tertiary, 'arrived': cs.primary, 'cancelled': cs.error, 'timeout': cs.onSurfaceVariant, }; return _ChartCard( title: 'قمع الرحلات (حالات الطلبات)', child: PieChart( PieChartData( sections: statuses.map((s) { final status = s['status']?.toString() ?? ''; final count = _toDouble(s['count']); return PieChartSectionData( value: count, title: '$status\n${count.toInt()}', titleStyle: const TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: Colors.white), color: colors[status] ?? cs.onSurfaceVariant, radius: 60, ); }).toList(), sectionsSpace: 2, centerSpaceRadius: 30, ), ), ); } } class _LayerChip extends StatelessWidget { final String label; final bool active; final Color color; final ValueChanged onChanged; const _LayerChip(this.label, this.active, this.color, this.onChanged); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return FilterChip( label: Text(label, style: TextStyle( fontSize: 12, color: active ? Colors.white : cs.onSurfaceVariant)), selected: active, onSelected: onChanged, selectedColor: color.withValues(alpha: 0.3), checkmarkColor: color, backgroundColor: cs.surfaceContainerHighest, side: BorderSide( color: active ? color.withValues(alpha: 0.5) : cs.outline), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ); } } // ═══════════════════════════════════════════════════════════════ // TAB 2: السوق والتسعير // ═══════════════════════════════════════════════════════════════ class _MarketTab extends StatelessWidget { final LiveAnalyticsController ctrl; const _MarketTab({required this.ctrl}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final mkt = ctrl.market; final countries = mkt['countries'] as Map? ?? {}; String latestPci = '—'; String latestShare = '—'; if (countries.isNotEmpty) { final firstRows = countries.values.first as List? ?? []; if (firstRows.isNotEmpty) { final last = firstRows.last; latestPci = _toDouble(last['average_pci']).toStringAsFixed(2); latestShare = '${_toDouble(last['market_share_percent']).toStringAsFixed(1)}%'; } } final pricingGrids = ctrl.pricing['grids'] as Map? ?? {}; return ListView( padding: const EdgeInsets.all(16), children: [ GridView.count( crossAxisCount: MediaQuery.of(context).size.width > 600 ? 3 : 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.8, children: [ _StatCard( label: 'مؤشر PCI الحالي', value: latestPci, sub: 'الهدف: 0.90 - 0.95', icon: Icons.price_change_rounded, ), _StatCard( label: 'الحصة السوقية', value: latestShare, icon: Icons.pie_chart_rounded, ), _StatCard( label: 'مربعات تسعيرية', value: pricingGrids.length.toString(), icon: Icons.grid_on_rounded, ), ], ), const SizedBox(height: 20), _buildCompetitorChart(cs), _buildPciChart(countries, cs), _buildMarketShareChart(countries, cs), ], ); } Widget _buildCompetitorChart(ColorScheme cs) { final hourly = (ctrl.competitor['hourly'] as List?) ?? []; if (hourly.isEmpty) return const SizedBox.shrink(); final competitors = {}; for (var r in hourly) { competitors.add(r['competitor_name']?.toString() ?? ''); } final compColors = [ cs.error, cs.tertiary, cs.tertiary, cs.success, cs.primary, ]; final hours = {}; for (var r in hourly) { hours.add(r['hour_bucket']?.toString().substring(11, 16) ?? ''); } final hourList = hours.toList(); return _ChartCard( title: 'أسعار المنافسين (آخر 24 ساعة)', height: 250, child: LineChart( LineChartData( lineBarsData: competitors.toList().asMap().entries.map((e) { final name = e.value; final spots = []; for (var i = 0; i < hourList.length; i++) { final row = hourly.firstWhere( (r) => r['competitor_name'] == name && (r['hour_bucket']?.toString().substring(11, 16) ?? '') == hourList[i], orElse: () => null, ); if (row != null) { spots.add(FlSpot(i.toDouble(), _toDouble(row['avg_price']))); } } return LineChartBarData( spots: spots, color: compColors[e.key % compColors.length], barWidth: 2, dotData: const FlDotData(show: false), isCurved: true, curveSmoothness: 0.3, ); }).toList(), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: (hourList.length / 6).ceilToDouble(), getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= hourList.length) return const SizedBox(); return Text(hourList[i], style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (_) => FlLine(color: cs.outline, strokeWidth: 0.5), ), borderData: FlBorderData(show: false), ), ), ); } Widget _buildPciChart(Map countries, ColorScheme cs) { if (countries.isEmpty) return const SizedBox.shrink(); final rows = (countries.values.first as List?) ?? []; if (rows.isEmpty) return const SizedBox.shrink(); return _ChartCard( title: 'مؤشر تنافسية الأسعار (PCI) - تاريخي', child: LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: rows.asMap().entries.map((e) { return FlSpot( e.key.toDouble(), _toDouble(e.value['average_pci'])); }).toList(), color: cs.primary, barWidth: 2, dotData: const FlDotData(show: true), isCurved: true, belowBarData: BarAreaData( show: true, color: cs.primary.withValues(alpha: 0.08), ), ), ], titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= rows.length) return const SizedBox(); final d = rows[i]['report_date']?.toString() ?? ''; return Text(d.length >= 10 ? d.substring(5) : d, style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), ), ), ); } Widget _buildMarketShareChart(Map countries, ColorScheme cs) { if (countries.isEmpty) return const SizedBox.shrink(); final rows = (countries.values.first as List?) ?? []; if (rows.isEmpty) return const SizedBox.shrink(); return _ChartCard( title: 'الحصة السوقية الأسبوعية', child: BarChart( BarChartData( barGroups: rows.asMap().entries.map((e) { return BarChartGroupData(x: e.key, barRods: [ BarChartRodData( toY: _toDouble(e.value['market_share_percent']), color: const Color(0xFF00CEC9).withValues(alpha: 0.7), width: 14, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ]); }).toList(), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= rows.length) return const SizedBox(); final d = rows[i]['report_date']?.toString() ?? ''; return Text(d.length >= 10 ? d.substring(5) : d, style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), ), ), ); } } // ═══════════════════════════════════════════════════════════════ // TAB 3: الأسطول والجودة // ═══════════════════════════════════════════════════════════════ class _FleetTab extends StatelessWidget { final LiveAnalyticsController ctrl; const _FleetTab({required this.ctrl}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final rt = ctrl.realtime; final complaintsList = (ctrl.complaints['by_type'] as List?) ?? []; final totalComplaints = complaintsList.fold(0, (s, c) => s + _toInt(c['count'])); final topZones = (ctrl.zones['zones'] as List?) ?? []; return ListView( padding: const EdgeInsets.all(16), children: [ GridView.count( crossAxisCount: MediaQuery.of(context).size.width > 600 ? 3 : 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.8, children: [ _StatCard( label: 'كباتن أونلاين', value: _fmt(rt['online_drivers']), icon: Icons.person_pin_circle_rounded, ), _StatCard( label: 'شكاوى مفتوحة', value: totalComplaints.toString(), icon: Icons.warning_amber_rounded, ), _StatCard( label: 'رخص تنتهي قريباً', value: _fmt(rt['expiring_licenses']), icon: Icons.badge_rounded, ), ], ), const SizedBox(height: 20), _buildComplaintsChart(complaintsList, cs), _buildRevenueProfitChart(cs), if (topZones.isNotEmpty) ...[ const _SectionTitle('أعلى المناطق نشاطاً (آخر 4 أسابيع)'), _buildZonesTable(topZones, cs), ], ], ); } Widget _buildComplaintsChart(List complaintsList, ColorScheme cs) { if (complaintsList.isEmpty) return const SizedBox.shrink(); final colors = [ cs.error, cs.tertiary, cs.tertiary, cs.primary, cs.success, cs.onSurfaceVariant, const Color(0xFFfd79a8), ]; return _ChartCard( title: 'توزيع الشكاوى حسب النوع', child: PieChart( PieChartData( sections: complaintsList.asMap().entries.map((e) { return PieChartSectionData( value: _toDouble(e.value['count']), title: '${e.value['complaint_type']}\n${_toInt(e.value['count'])}', titleStyle: const TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: Colors.white), color: colors[e.key % colors.length], radius: 65, ); }).toList(), sectionsSpace: 2, centerSpaceRadius: 25, ), ), ); } Widget _buildRevenueProfitChart(ColorScheme cs) { final daily = (ctrl.revenue['daily'] as List?) ?? []; if (daily.isEmpty) return const SizedBox.shrink(); return _ChartCard( title: 'الإيرادات اليومية vs أرباح الشركة', height: 250, child: LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: daily.asMap().entries.map((e) { return FlSpot( e.key.toDouble(), _toDouble(e.value['total_revenue'])); }).toList(), color: cs.tertiary, barWidth: 2, dotData: const FlDotData(show: false), isCurved: true, ), LineChartBarData( spots: daily.asMap().entries.map((e) { return FlSpot( e.key.toDouble(), _toDouble(e.value['company_profit'])); }).toList(), color: cs.success, barWidth: 2, dotData: const FlDotData(show: false), isCurved: true, ), ], titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: (daily.length / 6).ceilToDouble(), getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= daily.length) return const SizedBox(); final d = daily[i]['date']?.toString() ?? ''; return Text(d.length >= 10 ? d.substring(5) : d, style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (_) => FlLine(color: cs.outline, strokeWidth: 0.5), ), borderData: FlBorderData(show: false), ), ), ); } Widget _buildZonesTable(List topZones, ColorScheme cs) { return Container( decoration: BoxDecoration( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), border: Border.all(color: cs.outline), ), child: DataTable( columnSpacing: 20, headingRowColor: WidgetStateProperty.all(cs.surfaceContainerHighest), columns: [ const DataColumn( label: Text('#', style: TextStyle(fontSize: 12))), const DataColumn( label: Text('الموقع', style: TextStyle(fontSize: 12))), const DataColumn( label: Text('رحلات', style: TextStyle(fontSize: 12))), const DataColumn( label: Text('متوسط السعر', style: TextStyle(fontSize: 12))), ], rows: topZones.asMap().entries.map((e) { final z = e.value; return DataRow(cells: [ DataCell(Text('${e.key + 1}', style: const TextStyle( fontSize: 12))), DataCell(Text( '${_toDouble(z['lat']).toStringAsFixed(4)}, ${_toDouble(z['lng']).toStringAsFixed(4)}', style: const TextStyle( fontSize: 12))), DataCell(Text(_fmt(z['rides']), style: const TextStyle( fontSize: 12))), DataCell(Text(_toDouble(z['avg_price']).toStringAsFixed(2), style: const TextStyle( fontSize: 12))), ]); }).toList(), ), ); } } // ═══════════════════════════════════════════════════════════════ // TAB 4: النمو والاحتفاظ // ═══════════════════════════════════════════════════════════════ class _GrowthTab extends StatelessWidget { final LiveAnalyticsController ctrl; const _GrowthTab({required this.ctrl}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final gr = ctrl.growth; final totals = gr['totals'] as Map? ?? {}; final weeks = (ctrl.weekly['weeks'] as List?) ?? []; String weekChange = ''; Color weekColor = cs.onSurfaceVariant; if (weeks.length >= 2) { final last = _toInt(weeks.last['rides']); final prev = _toInt(weeks[weeks.length - 2]['rides']); if (prev > 0) { final pct = ((last - prev) / prev * 100).toStringAsFixed(1); weekChange = '${double.parse(pct) >= 0 ? '+' : ''}$pct% عن الأسبوع السابق'; weekColor = double.parse(pct) >= 0 ? cs.success : cs.error; } } return ListView( padding: const EdgeInsets.all(16), children: [ GridView.count( crossAxisCount: MediaQuery.of(context).size.width > 600 ? 3 : 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.8, children: [ _StatCard( label: 'إجمالي الركاب', value: _fmt(totals['passengers']), icon: Icons.people_rounded, ), _StatCard( label: 'إجمالي الكباتن', value: _fmt(totals['drivers']), icon: Icons.drive_eta_rounded, ), if (weeks.isNotEmpty) _StatCard( label: 'رحلات آخر أسبوع', value: _fmt(weeks.last['rides']), sub: weekChange, subColor: weekColor, icon: Icons.trending_up_rounded, ), ], ), const SizedBox(height: 20), _buildGrowthChart(cs), _buildWeeklyChart(weeks, cs), _buildRetentionChart(cs), _buildRevenueChart(cs), ], ); } Widget _buildGrowthChart(ColorScheme cs) { final passengers = (ctrl.growth['passengers'] as List?) ?? []; final drivers = (ctrl.growth['drivers'] as List?) ?? []; if (passengers.isEmpty && drivers.isEmpty) return const SizedBox.shrink(); final maxLen = passengers.length > drivers.length ? passengers.length : drivers.length; return _ChartCard( title: 'نمو المسجّلين الجدد (30 يوم)', height: 250, child: LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: passengers.asMap().entries.map((e) { return FlSpot(e.key.toDouble(), _toDouble(e.value['count'])); }).toList(), color: cs.primary, barWidth: 2, dotData: const FlDotData(show: false), isCurved: true, belowBarData: BarAreaData( show: true, color: cs.primary.withValues(alpha: 0.08), ), ), LineChartBarData( spots: drivers.asMap().entries.map((e) { return FlSpot(e.key.toDouble(), _toDouble(e.value['count'])); }).toList(), color: const Color(0xFF00CEC9), barWidth: 2, dotData: const FlDotData(show: false), isCurved: true, belowBarData: BarAreaData( show: true, color: const Color(0xFF00CEC9).withValues(alpha: 0.08), ), ), ], titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: (maxLen / 6).ceilToDouble(), getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= passengers.length) return const SizedBox(); final d = passengers[i]['date']?.toString() ?? ''; return Text(d.length >= 10 ? d.substring(5) : d, style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), ), ), ); } Widget _buildWeeklyChart(List weeks, ColorScheme cs) { if (weeks.isEmpty) return const SizedBox.shrink(); return _ChartCard( title: 'مقارنة أسبوع بأسبوع', child: BarChart( BarChartData( barGroups: weeks.asMap().entries.map((e) { return BarChartGroupData(x: e.key, barRods: [ BarChartRodData( toY: _toDouble(e.value['rides']), color: cs.primary.withValues(alpha: 0.7), width: 12, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ]); }).toList(), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= weeks.length) return const SizedBox(); final d = weeks[i]['week_start']?.toString() ?? ''; return Text(d.length >= 10 ? d.substring(5) : d, style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), ), ), ); } Widget _buildRetentionChart(ColorScheme cs) { final cohorts = (ctrl.retention['cohorts'] as List?) ?? []; if (cohorts.isEmpty) return const SizedBox.shrink(); return _ChartCard( title: 'احتفاظ الركاب (Retention Cohort)', child: BarChart( BarChartData( barGroups: cohorts.asMap().entries.map((e) { final opacity = (1.0 - e.key * 0.05).clamp(0.3, 1.0); return BarChartGroupData(x: e.key, barRods: [ BarChartRodData( toY: _toDouble(e.value['active_passengers']), color: cs.primary.withValues(alpha: opacity), width: 14, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), ]); }).toList(), titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (v, _) { return Text('w${v.toInt()}', style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: const FlGridData(show: false), borderData: FlBorderData(show: false), ), ), ); } Widget _buildRevenueChart(ColorScheme cs) { final daily = (ctrl.revenue['daily'] as List?) ?? []; if (daily.isEmpty) return const SizedBox.shrink(); return _ChartCard( title: 'الإيرادات اليومية (30 يوم)', height: 250, child: LineChart( LineChartData( lineBarsData: [ LineChartBarData( spots: daily.asMap().entries.map((e) { return FlSpot( e.key.toDouble(), _toDouble(e.value['total_revenue'])); }).toList(), color: cs.tertiary, barWidth: 2, dotData: const FlDotData(show: false), isCurved: true, belowBarData: BarAreaData( show: true, color: cs.tertiary.withValues(alpha: 0.08), ), ), ], titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: (daily.length / 6).ceilToDouble(), getTitlesWidget: (v, _) { final i = v.toInt(); if (i < 0 || i >= daily.length) return const SizedBox(); final d = daily[i]['date']?.toString() ?? ''; return Text(d.length >= 10 ? d.substring(5) : d, style: TextStyle( fontSize: 9, color: cs.onSurfaceVariant)); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false)), ), gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (_) => FlLine(color: cs.outline, strokeWidth: 0.5), ), borderData: FlBorderData(show: false), ), ), ); } }