import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart' hide TextDirection; import 'package:sefer_admin1/controller/admin/static_controller.dart'; import 'notes_driver_page.dart'; // ══════════════════════════════════════════════════════════════ // DESIGN TOKENS // ══════════════════════════════════════════════════════════════ const Color _bg = Color(0xFF0D1117); const Color _surface = Color(0xFF161B22); const Color _surfaceElevated = Color(0xFF1C2333); const Color _accent = Color(0xFF00D4AA); const Color _danger = Color(0xFFFF5370); const Color _warning = Color(0xFFFFCB6B); const Color _info = Color(0xFF82AAFF); const Color _purple = Color(0xFFC792EA); const Color _textPrimary = Color(0xFFE6EDF3); const Color _textSecondary = Color(0xFF7D8590); const Color _divider = Color(0xFF21262D); class StaticDash extends StatelessWidget { const StaticDash({super.key}); @override Widget build(BuildContext context) { final controller = Get.put(StaticController()); return Scaffold( backgroundColor: _bg, body: GetBuilder( builder: (c) { return CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // ── App Bar ──────────────────────────────────── _SliverHeader(controller: c), if (c.isLoading) const SliverFillRemaining(child: _LoadingState()) else ...[ // ── Date Badge ───────────────────────────── SliverToBoxAdapter( child: _DateBadge(controller: c), ), // ── KPI Row ──────────────────────────────── SliverToBoxAdapter( child: _KpiRow(controller: c), ), // ── Section: Growth ──────────────────────── const SliverToBoxAdapter( child: _SectionLabel( 'نظرة عامة على النمو', Icons.trending_up_rounded, ), ), SliverToBoxAdapter( child: AnimationLimiter( child: Column( children: AnimationConfiguration.toStaggeredList( duration: const Duration(milliseconds: 450), childAnimationBuilder: (w) => SlideAnimation( verticalOffset: 40, child: FadeInAnimation(child: w), ), children: [ _DarkChartCard( title: 'الركاب', total: c.totalMonthlyPassengers, spots: c.chartDataPassengers, compareSpots: c.isComparing ? c.chartDataPassengersCompare : null, color: _info, icon: Icons.groups_rounded, controller: c, ), const SizedBox(height: 12), _DarkChartCard( title: 'السائقون', total: c.totalMonthlyDrivers, spots: c.chartDataDrivers, compareSpots: c.isComparing ? c.chartDataDriversCompare : null, color: _warning, icon: Icons.drive_eta_rounded, controller: c, ), const SizedBox(height: 12), _DarkChartCard( title: 'الرحلات', total: c.totalMonthlyRides, spots: c.chartDataRides, compareSpots: c.isComparing ? c.chartDataRidesCompare : null, color: _purple, icon: Icons.map_rounded, controller: c, ), const SizedBox(height: 24), ], ), ), ), ), // ── Section: Team Performance ────────────── const SliverToBoxAdapter( child: _SectionLabel( 'أداء فريق العمل', Icons.workspaces_filled, ), ), SliverToBoxAdapter( child: Column( children: [ // Activations _DynamicMultiLineCard( title: 'تفعيل السائقين', subtitle: 'Activations', icon: Icons.how_to_reg_rounded, controller: c, getSpots: (emp) => emp.notesSpots, getCompareSpots: (emp) => c.employeeDataCompare[emp.name]?.notesSpots ?? [], getTotal: (emp) => emp.totalNotes, ), const SizedBox(height: 12), // Calls _DynamicMultiLineCard( title: 'عدد المكالمات', subtitle: 'Calls', icon: Icons.phone_in_talk_rounded, controller: c, getSpots: (emp) => emp.callsSpots, getCompareSpots: (emp) => c.employeeDataCompare[emp.name]?.callsSpots ?? [], getTotal: (emp) => emp.totalCalls, onTap: () => Get.to(() => const DailyNotesView()), ), const SizedBox(height: 24), ], ), ), // ── Section: Employee Leaderboard ────────── if (c.employmentStatsList.isNotEmpty) ...[ const SliverToBoxAdapter( child: _SectionLabel( 'لوحة الصدارة', Icons.emoji_events_rounded, ), ), SliverToBoxAdapter( child: _EmployeeLeaderboard(stats: c.employmentStatsList), ), const SliverToBoxAdapter(child: SizedBox(height: 24)), ], // ── Section: Registration Follow-up ──────── const SliverToBoxAdapter( child: _SectionLabel( 'متابعة التسجيل', Icons.assignment_turned_in_rounded, ), ), SliverToBoxAdapter( child: Column( children: [ _DarkChartCard( title: 'سائقين بعد الاتصال', total: c.staticList.isNotEmpty && c.staticList[0]['totalMonthlyMatchingNotes'] != null ? c.staticList[0]['totalMonthlyMatchingNotes'] .toString() : '0', spots: c.chartDataDriversMatchingNotes, compareSpots: c.isComparing ? c.chartDataDriversMatchingNotesCompare : null, color: _accent, icon: Icons.phone_in_talk_rounded, controller: c, ), const SizedBox(height: 60), ], ), ), ], ], ); }, ), ); } } // ══════════════════════════════════════════════════════════════ // SLIVER HEADER // ══════════════════════════════════════════════════════════════ class _SliverHeader extends StatelessWidget { final StaticController controller; const _SliverHeader({required this.controller}); @override Widget build(BuildContext context) { return SliverAppBar( expandedHeight: 100, pinned: true, floating: true, backgroundColor: _bg, elevation: 0, leading: GestureDetector( onTap: () => Get.back(), child: Container( margin: const EdgeInsets.all(10), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(10), border: Border.all(color: _divider), ), child: const Icon(Icons.arrow_back_ios_new_rounded, color: _textSecondary, size: 16), ), ), flexibleSpace: FlexibleSpaceBar( background: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF0F1F1A), _bg], ), ), child: Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _accent.withOpacity(0.12), borderRadius: BorderRadius.circular(10), border: Border.all(color: _accent.withOpacity(0.25)), ), child: const Icon(Icons.bar_chart_rounded, color: _accent, size: 18), ), const SizedBox(width: 10), ShaderMask( shaderCallback: (b) => const LinearGradient( colors: [_accent, _info], ).createShader(b), child: const Text( 'لوحة الإحصائيات', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700, ), ), ), ], ), ), ), ), ), actions: [ GestureDetector( onTap: () async => await controller.getAll(), child: Container( margin: const EdgeInsets.only(right: 16, top: 10, bottom: 10), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(10), border: Border.all(color: _divider), ), child: const Icon(Icons.refresh_rounded, color: _textSecondary, size: 18), ), ), ], bottom: PreferredSize( preferredSize: const Size.fromHeight(1), child: Container(height: 1, color: _divider), ), ); } } // ══════════════════════════════════════════════════════════════ // DATE BADGE // ══════════════════════════════════════════════════════════════ class _DateBadge extends StatelessWidget { final StaticController controller; const _DateBadge({required this.controller}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: _divider), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.calendar_today_rounded, color: _accent, size: 13), const SizedBox(width: 7), Text( controller.currentDateString, style: const TextStyle( color: _textSecondary, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), if (controller.isComparing) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.grey.withOpacity(0.2)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: const BoxDecoration( shape: BoxShape.circle, color: _textSecondary, ), ), const SizedBox(width: 7), Text( controller.compareDateString, style: const TextStyle( color: _textSecondary, fontSize: 12, fontWeight: FontWeight.w500), ), ], ), ), ] ], ), ); } } // ══════════════════════════════════════════════════════════════ // KPI ROW // ══════════════════════════════════════════════════════════════ class _KpiRow extends StatelessWidget { final StaticController controller; const _KpiRow({required this.controller}); @override Widget build(BuildContext context) { final items = [ _KpiItem('الركاب', controller.totalMonthlyPassengers, Icons.groups_rounded, _info), _KpiItem('السائقون', controller.totalMonthlyDrivers, Icons.drive_eta_rounded, _warning), _KpiItem( 'الرحلات', controller.totalMonthlyRides, Icons.map_rounded, _purple), ]; return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Row( children: items .map((item) => Expanded( child: Padding( padding: EdgeInsets.only(right: item != items.last ? 8 : 0), child: _buildKpiCard(item), ), )) .toList(), ), ); } Widget _buildKpiCard(_KpiItem item) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: item.color.withOpacity(0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(item.icon, color: item.color, size: 18), const SizedBox(height: 8), Text( item.value, style: const TextStyle( color: _textPrimary, fontSize: 20, fontWeight: FontWeight.w800, height: 1, ), ), const SizedBox(height: 3), Text(item.label, style: const TextStyle(color: _textSecondary, fontSize: 10)), ], ), ); } } class _KpiItem { final String label, value; final IconData icon; final Color color; const _KpiItem(this.label, this.value, this.icon, this.color); } // ══════════════════════════════════════════════════════════════ // SECTION LABEL // ══════════════════════════════════════════════════════════════ class _SectionLabel extends StatelessWidget { final String text; final IconData icon; const _SectionLabel(this.text, this.icon); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 12), child: Row( children: [ Container( width: 3, height: 16, decoration: BoxDecoration( color: _accent, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 10), Icon(icon, size: 16, color: _textSecondary), const SizedBox(width: 8), Text( text, style: const TextStyle( color: _textPrimary, fontSize: 15, fontWeight: FontWeight.w700, letterSpacing: 0.3, ), ), ], ), ); } } // ══════════════════════════════════════════════════════════════ // DARK SINGLE-LINE CHART CARD // ══════════════════════════════════════════════════════════════ class _DarkChartCard extends StatelessWidget { final String title, total; final List spots; final List? compareSpots; final Color color; final IconData icon; final StaticController controller; const _DarkChartCard({ required this.title, required this.total, required this.spots, this.compareSpots, required this.color, required this.icon, required this.controller, }); @override Widget build(BuildContext context) { final allSpots = [...spots, ...?compareSpots]; final maxY = _maxY(allSpots); final interval = _interval(maxY); final days = _days(controller); final xInterval = _xInterval(days); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Container( height: 340, decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withOpacity(0.15)), boxShadow: [ BoxShadow( color: color.withOpacity(0.06), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( children: [ // Header Padding( padding: const EdgeInsets.all(18), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ Container( padding: const EdgeInsets.all(9), decoration: BoxDecoration( color: color.withOpacity(0.12), borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.25)), ), child: Icon(icon, color: color, size: 18), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( color: _textPrimary, fontSize: 14, fontWeight: FontWeight.w700)), Text('الإجمالي: $total', style: const TextStyle( color: _textSecondary, fontSize: 11)), ]), ]), if (controller.isComparing) Row(children: [ Container( width: 16, height: 2, decoration: BoxDecoration( color: _textSecondary, borderRadius: BorderRadius.circular(1), )), const SizedBox(width: 5), const Text('سابق', style: TextStyle(color: _textSecondary, fontSize: 10)), ]), ], ), ), // Chart Expanded( child: Padding( padding: const EdgeInsets.fromLTRB(4, 0, 12, 10), child: Directionality( textDirection: TextDirection.ltr, child: LineChart(LineChartData( minX: 1, maxX: days > 0 ? days : 1, minY: 0, maxY: maxY, lineTouchData: _tooltipData(controller, color), gridData: _gridData(interval), titlesData: _titlesData(interval, xInterval, days, controller), borderData: FlBorderData(show: false), lineBarsData: [ if (compareSpots != null && compareSpots!.isNotEmpty) _line(compareSpots!, _textSecondary, isDashed: true, isStep: true, width: 1.5), _line(spots, color, isStep: true, width: 2.5), ], )), ), ), ), // Control Bar _ControlBar(controller: controller, accentColor: color), ], ), ), ); } } // ══════════════════════════════════════════════════════════════ // DYNAMIC MULTI-LINE CHART CARD 🔥 Uses dynamic employee data // ══════════════════════════════════════════════════════════════ class _DynamicMultiLineCard extends StatelessWidget { final String title, subtitle; final IconData icon; final StaticController controller; final List Function(EmployeeChartData) getSpots; final List Function(EmployeeChartData) getCompareSpots; final int Function(EmployeeChartData) getTotal; final VoidCallback? onTap; const _DynamicMultiLineCard({ required this.title, required this.subtitle, required this.icon, required this.controller, required this.getSpots, required this.getCompareSpots, required this.getTotal, this.onTap, }); @override Widget build(BuildContext context) { final employees = controller.employeeData.values.toList(); // Sort by total descending for legend ordering employees.sort((a, b) => getTotal(b).compareTo(getTotal(a))); final allSpots = employees.expand((e) => getSpots(e)).toList(); final maxY = _maxY(allSpots); final interval = _interval(maxY); final days = _days(controller); final xInterval = _xInterval(days); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Container( decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: _divider), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 15, offset: const Offset(0, 6), ), ], ), child: Column( children: [ // ── Header ────────────────────────────────────── InkWell( onTap: onTap, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), child: Padding( padding: const EdgeInsets.all(18), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ Container( padding: const EdgeInsets.all(9), decoration: BoxDecoration( color: _accent.withOpacity(0.10), borderRadius: BorderRadius.circular(10), border: Border.all(color: _accent.withOpacity(0.20)), ), child: Icon(icon, color: _accent, size: 18), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( color: _textPrimary, fontSize: 14, fontWeight: FontWeight.w700)), Text(subtitle, style: const TextStyle( color: _textSecondary, fontSize: 11)), if (onTap != null) const Text( 'اضغط لعرض التفاصيل اليومية', style: TextStyle( color: _accent, fontSize: 9, fontWeight: FontWeight.w600), ), ]), ]), if (onTap != null) const Icon(Icons.arrow_forward_ios_rounded, size: 14, color: _textSecondary), ], ), ), ), // ── Dynamic Legend ────────────────────────────── if (employees.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Text('لا توجد بيانات', style: TextStyle(color: _textSecondary)), ) else ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 4), child: Wrap( spacing: 10, runSpacing: 8, children: employees.map((emp) { return _LegendChip( name: emp.name.toUpperCase(), color: emp.color, total: getTotal(emp), ); }).toList(), ), ), const SizedBox(height: 8), // ── Chart ─────────────────────────────────── SizedBox( height: 240, child: Padding( padding: const EdgeInsets.fromLTRB(4, 0, 12, 10), child: Directionality( textDirection: TextDirection.ltr, child: LineChart(LineChartData( minX: 1, maxX: days > 0 ? days : 1, minY: 0, maxY: maxY, lineTouchData: _tooltipData(controller, _accent), gridData: _gridData(interval), titlesData: _titlesData(interval, xInterval, days, controller), borderData: FlBorderData(show: false), lineBarsData: [ // Compare lines (dashed) if (controller.isComparing) ...employees.map((emp) => _line( getCompareSpots(emp), emp.color.withOpacity(0.25), isDashed: true, isStep: false, width: 1.5, )), // Main lines ...employees.map((emp) => _line( getSpots(emp), emp.color, isStep: false, width: 2.5, )), ], )), ), ), ), ], _ControlBar(controller: controller, accentColor: _accent), ], ), ), ); } } // ══════════════════════════════════════════════════════════════ // LEGEND CHIP // ══════════════════════════════════════════════════════════════ class _LegendChip extends StatelessWidget { final String name; final Color color; final int total; const _LegendChip( {required this.name, required this.color, required this.total}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withOpacity(0.25)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 7, height: 7, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text(name, style: TextStyle( color: color, fontSize: 10, fontWeight: FontWeight.w700)), const SizedBox(width: 5), Text('$total', style: const TextStyle( color: _textSecondary, fontSize: 10, fontWeight: FontWeight.w500)), ], ), ); } } // ══════════════════════════════════════════════════════════════ // EMPLOYEE LEADERBOARD 🔥 Fully dynamic // ══════════════════════════════════════════════════════════════ class _EmployeeLeaderboard extends StatelessWidget { final List stats; const _EmployeeLeaderboard({required this.stats}); @override Widget build(BuildContext context) { final maxCount = stats.isEmpty ? 1 : stats.first.count; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: _divider), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _warning.withOpacity(0.12), borderRadius: BorderRadius.circular(9), border: Border.all(color: _warning.withOpacity(0.25)), ), child: const Icon(Icons.emoji_events_rounded, color: _warning, size: 16), ), const SizedBox(width: 10), const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('لوحة الصدارة', style: TextStyle( color: _textPrimary, fontSize: 14, fontWeight: FontWeight.w700)), Text('إجمالي الإدخالات حسب الموظف', style: TextStyle(color: _textSecondary, fontSize: 10)), ], ), ]), Text( '${stats.fold(0, (s, e) => s + e.count)} إجمالي', style: const TextStyle( color: _textSecondary, fontSize: 11, fontWeight: FontWeight.w600), ), ], ), const SizedBox(height: 20), // Rank rows ...List.generate(stats.length, (i) { final stat = stats[i]; final pct = maxCount > 0 ? stat.count / maxCount : 0.0; return Padding( padding: const EdgeInsets.only(bottom: 14), child: Column( children: [ Row( children: [ // Rank Badge _RankBadge(rank: i + 1, color: stat.color), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( stat.name.toUpperCase(), style: TextStyle( color: stat.color, fontSize: 12, fontWeight: FontWeight.w700, ), ), Text( '${stat.count}', style: const TextStyle( color: _textPrimary, fontSize: 14, fontWeight: FontWeight.w800, fontFamily: 'monospace', ), ), ], ), const SizedBox(height: 6), // Progress bar ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: pct.toDouble(), backgroundColor: _divider, valueColor: AlwaysStoppedAnimation(stat.color), minHeight: 5, ), ), ], ), ), ], ), ], ), ); }), ], ), ), ); } } class _RankBadge extends StatelessWidget { final int rank; final Color color; const _RankBadge({required this.rank, required this.color}); @override Widget build(BuildContext context) { final isTop3 = rank <= 3; final medalColors = [_warning, _textSecondary, const Color(0xFFCD7F32)]; return Container( width: 32, height: 32, decoration: BoxDecoration( color: isTop3 ? medalColors[rank - 1].withOpacity(0.12) : _divider, shape: BoxShape.circle, border: Border.all( color: isTop3 ? medalColors[rank - 1].withOpacity(0.4) : Colors.transparent), ), child: Center( child: Text( '$rank', style: TextStyle( color: isTop3 ? medalColors[rank - 1] : _textSecondary, fontSize: 12, fontWeight: FontWeight.w800, ), ), ), ); } } // ══════════════════════════════════════════════════════════════ // SHARED CONTROL BAR // ══════════════════════════════════════════════════════════════ class _ControlBar extends StatelessWidget { final StaticController controller; final Color accentColor; const _ControlBar({required this.controller, required this.accentColor}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: _bg, borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)), border: const Border(top: BorderSide(color: _divider)), ), child: Row( children: [ Expanded( child: InkWell( onTap: () async { final now = DateTime.now(); final currentEnd = controller.endDate ?? now; final lastDate = currentEnd.isAfter(now) ? currentEnd.add(const Duration(days: 1)) : now.add(const Duration(days: 1)); final picked = await showDateRangePicker( context: context, firstDate: DateTime(2020), lastDate: lastDate, initialDateRange: DateTimeRange( start: controller.startDate ?? now.subtract(const Duration(days: 30)), end: currentEnd, ), builder: (ctx, child) => Theme( data: Theme.of(ctx).copyWith( colorScheme: const ColorScheme.dark( primary: _accent, onPrimary: _bg, surface: _surfaceElevated, onSurface: _textPrimary, ), ), child: child!, ), ); if (picked != null) { controller.updateDateRange(picked.start, picked.end); } }, borderRadius: BorderRadius.circular(9), child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(9), border: Border.all(color: _divider), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.calendar_today_rounded, size: 13, color: accentColor), const SizedBox(width: 7), Text( _formatRange(controller), style: const TextStyle( color: _textSecondary, fontSize: 11, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ], ), ), ), ), const SizedBox(width: 10), InkWell( onTap: controller.toggleComparison, borderRadius: BorderRadius.circular(9), child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: controller.isComparing ? _danger.withOpacity(0.10) : _surface, borderRadius: BorderRadius.circular(9), border: Border.all( color: controller.isComparing ? _danger.withOpacity(0.3) : _divider), ), child: Row( children: [ Icon( controller.isComparing ? Icons.visibility_off_outlined : Icons.compare_arrows_rounded, size: 14, color: controller.isComparing ? _danger : accentColor, ), const SizedBox(width: 5), Text( controller.isComparing ? 'إلغاء' : 'مقارنة', style: TextStyle( color: controller.isComparing ? _danger : accentColor, fontSize: 11, fontWeight: FontWeight.w600, ), ), ], ), ), ), ], ), ); } String _formatRange(StaticController c) { if (c.startDate == null || c.endDate == null) return c.currentDateString; return "${c.startDate!.day}/${c.startDate!.month}/${c.startDate!.year}" " - ${c.endDate!.day}/${c.endDate!.month}/${c.endDate!.year}"; } } // ══════════════════════════════════════════════════════════════ // LOADING STATE // ══════════════════════════════════════════════════════════════ class _LoadingState extends StatelessWidget { const _LoadingState(); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 40, height: 40, child: CircularProgressIndicator( color: _accent, strokeWidth: 2, backgroundColor: _accent.withOpacity(0.1), ), ), const SizedBox(height: 16), const Text('جاري تحميل الإحصائيات...', style: TextStyle(color: _textSecondary, fontSize: 13)), ], ), ); } } // ══════════════════════════════════════════════════════════════ // CHART HELPERS (pure functions) // ══════════════════════════════════════════════════════════════ LineChartBarData _line( List spots, Color color, { bool isDashed = false, bool isStep = false, double width = 2.5, }) { return LineChartBarData( spots: spots, isCurved: !isStep, curveSmoothness: 0.25, isStepLineChart: isStep, color: color, barWidth: width, isStrokeCapRound: !isStep, dotData: const FlDotData(show: false), dashArray: isDashed ? [5, 5] : null, lineChartStepData: const LineChartStepData(stepDirection: 0.5), belowBarData: BarAreaData( show: !isDashed, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [color.withOpacity(0.12), color.withOpacity(0.0)], ), ), ); } LineTouchData _tooltipData(StaticController c, Color accentColor) { return LineTouchData( handleBuiltInTouches: true, touchTooltipData: LineTouchTooltipData( getTooltipColor: (_) => _surfaceElevated, tooltipBorder: const BorderSide(color: _divider), tooltipPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), tooltipRoundedRadius: 10, getTooltipItems: (spots) { return spots.map((barSpot) { final start = c.startDate; String dateStr = ''; if (start != null) { final date = start.add(Duration(days: barSpot.x.toInt() - 1)); dateStr = DateFormat('d MMM', 'en').format(date); } return LineTooltipItem( '$dateStr\n', const TextStyle( color: _textSecondary, fontWeight: FontWeight.w600, fontSize: 11), children: [ TextSpan( text: barSpot.y.toInt().toString(), style: TextStyle( color: barSpot.bar.color, fontWeight: FontWeight.w900, fontSize: 15), ), ], ); }).toList(); }, ), ); } FlGridData _gridData(double interval) { return FlGridData( show: true, drawVerticalLine: false, horizontalInterval: interval, getDrawingHorizontalLine: (_) => const FlLine(color: Color(0xFF21262D), strokeWidth: 1), ); } FlTitlesData _titlesData( double interval, double xInterval, double days, StaticController c, ) { return FlTitlesData( show: true, rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 28, interval: xInterval, getTitlesWidget: (value, _) { if (value <= 0 || value > days) return const SizedBox.shrink(); if (c.startDate == null) return const SizedBox.shrink(); final date = c.startDate!.add(Duration(days: value.toInt() - 1)); return Padding( padding: const EdgeInsets.only(top: 6), child: Text( DateFormat('d/M', 'en').format(date), style: const TextStyle( color: _textSecondary, fontSize: 9, fontWeight: FontWeight.w600), ), ); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, interval: interval, reservedSize: 36, getTitlesWidget: (value, _) => Text( _formatNum(value), style: const TextStyle(color: _textSecondary, fontSize: 9), ), ), ), ); } double _maxY(List spots) { if (spots.isEmpty) return 10; final max = spots.map((s) => s.y).reduce((a, b) => a > b ? a : b); return max <= 0 ? 5 : (max * 1.2).ceilToDouble(); } double _interval(double maxY) { if (maxY <= 10) return 2; if (maxY <= 50) return 10; if (maxY <= 100) return 20; if (maxY <= 500) return 100; if (maxY <= 1000) return 200; return maxY / 5; } double _days(StaticController c) { if (c.startDate == null || c.endDate == null) return 30; return c.endDate!.difference(c.startDate!).inDays + 1.0; } double _xInterval(double days) { if (days > 60) return 30; if (days > 30) return 10; if (days > 10) return 5; return 1; } String _formatNum(double v) { if (v >= 1000) return '${(v / 1000).toStringAsFixed(1)}k'; return v.toInt().toString(); }