import 'package:flutter/material.dart'; import '../about/internal/application/TextType.dart'; import '../about/internal/application/DeploymentRecord.dart'; import '../utils/Colors.dart'; import 'text/Text.dart'; enum _Range { week, month, year } class _Bucket { final String label; final int count; _Bucket(this.label, this.count); } /// "Compute Activity" style card: a bar chart of deployment counts over time, /// with the peak column highlighted, and a Week / Month / Year toggle. class ActivityChart extends StatefulWidget { final List history; const ActivityChart({super.key, required this.history}); @override State createState() => _ActivityChartState(); } class _ActivityChartState extends State { _Range _range = _Range.week; List<_Bucket> _buckets() { final now = DateTime.now(); final h = widget.history; int between(DateTime a, DateTime b) => h.where((r) => !r.time.isBefore(a) && r.time.isBefore(b)).length; const wd = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const mo = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; switch (_range) { case _Range.week: final today = DateTime(now.year, now.month, now.day); return List.generate(7, (i) { final d = today.subtract(Duration(days: 6 - i)); return _Bucket( wd[d.weekday - 1], between(d, d.add(const Duration(days: 1))), ); }); case _Range.month: // last 6 weeks final startOfWeek = DateTime( now.year, now.month, now.day, ).subtract(Duration(days: now.weekday - 1)); return List.generate(6, (i) { final s = startOfWeek.subtract(Duration(days: (5 - i) * 7)); final e = s.add(const Duration(days: 7)); return _Bucket('${s.month}/${s.day}', between(s, e)); }); case _Range.year: // last 6 months return List.generate(6, (i) { final m = DateTime(now.year, now.month - (5 - i), 1); final next = DateTime(m.year, m.month + 1, 1); return _Bucket(mo[m.month - 1], between(m, next)); }); } } @override Widget build(BuildContext context) { final jp = context.jp; final buckets = _buckets(); final max = buckets.fold(0, (m, b) => b.count > m ? b.count : m); final peakIdx = buckets.indexWhere((b) => b.count == max && max > 0); final total = buckets.fold(0, (n, b) => n + b.count); return Container( padding: const EdgeInsets.all(18), decoration: PeachTheme.panel(context), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ text( 'Deployment activity', 16, TextType.Bold, color: jp.text, weight: FontWeight.w800, ), const SizedBox(height: 2), text( '$total deploys this period', 12.5, TextType.Regular, color: jp.textDim, weight: FontWeight.w400, ), ], ), ), _toggle(jp), ], ), const SizedBox(height: 20), SizedBox( height: 190, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ for (var i = 0; i < buckets.length; i++) Expanded(child: _bar(jp, buckets[i], max, i == peakIdx)), ], ), ), ], ), ); } Widget _bar(JpColors jp, _Bucket b, int max, bool peak) { final frac = max == 0 ? 0.0 : b.count / max; final h = 12 + frac * 130; return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ text( '${b.count}', 12, TextType.Regular, color: peak ? PeachTheme.seed : jp.textDim, weight: FontWeight.w800, ), const SizedBox(height: 6), Container( width: 26, height: h, decoration: BoxDecoration( gradient: peak ? PeachTheme.brandGradient : null, color: peak ? null : jp.surface2, borderRadius: BorderRadius.circular(9), boxShadow: peak ? PeachTheme.glow(PeachTheme.seed, opacity: 0.35, blur: 16) : null, ), ), const SizedBox(height: 8), text( b.label, 11, TextType.Regular, color: peak ? jp.text : jp.textFaint, maxLines: 1, overflow: TextOverflow.clip, weight: peak ? FontWeight.w700 : FontWeight.w500, ), ], ); } Widget _toggle(JpColors jp) { Widget seg(_Range r, String label) { final active = _range == r; return GestureDetector( onTap: () => setState(() => _range = r), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), decoration: BoxDecoration( color: active ? jp.surface : Colors.transparent, borderRadius: BorderRadius.circular(9), boxShadow: active ? [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 6, offset: const Offset(0, 2), ), ] : null, ), child: text( label, 12.5, TextType.Regular, color: active ? jp.text : jp.textDim, weight: active ? FontWeight.w700 : FontWeight.w500, ), ), ); } return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: jp.surface2, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ seg(_Range.week, 'Week'), seg(_Range.month, 'Month'), seg(_Range.year, 'Year'), ], ), ); } }