diff --git a/lib/interfaces/localstore.dart b/lib/interfaces/localstore.dart index 008ae9f..6ddb43f 100644 --- a/lib/interfaces/localstore.dart +++ b/lib/interfaces/localstore.dart @@ -3,15 +3,20 @@ import 'dart:async'; import 'package:localstore/localstore.dart'; import 'package:stock/stock.dart'; +import '../pages/matchscout.dart' show MatchScoutInfoSerialized; import 'bluealliance.dart'; class LocalStoreInterface { static final _db = Localstore.instance; - static Future addMatch(int season, Map data) => _db + static Future addMatch(MatchScoutInfoSerialized key, Map fields) => _db .collection("scout") - .doc("match-$season${data['event']}_${data['match']}-${data['team']}") - .set(data..['season'] = season); + .doc("match-${key.season}${key.event}_${key.match}-${key.team}") + .set(fields + ..['season'] = key.season + ..['event'] = key.event + ..['match'] = key.match + ..['team'] = key.team); static Future addPit(int season, Map data) => _db .collection("scout") diff --git a/lib/interfaces/supabase.dart b/lib/interfaces/supabase.dart index 2ddec9e..9e28c19 100644 --- a/lib/interfaces/supabase.dart +++ b/lib/interfaces/supabase.dart @@ -5,6 +5,9 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import '../pages/configuration.dart'; import '../pages/matchscout.dart' hide MatchScoutPage; +import 'bluealliance.dart' show MatchInfo, parseMatchInfo; + +typedef AggInfo = ({int season, String? event, String? team}); // "Aren't supabase functions all over the code?" Yes, but here are the ones that require big think (and big caching) class SupabaseInterface { @@ -48,9 +51,8 @@ class SupabaseInterface { static final matchscoutStock = Stock( fetcher: Fetcher.ofFuture((season) => Supabase.instance.client - .rpc('gettableschema', params: {"tablename": "${season}_match"}).then((resp) { - var schema = (Map.from(resp) - ..removeWhere((key, _) => {"event", "match", "team", "scouter"}.contains(key))) + .rpc('gettableschema', params: {"tablename": "match_data_$season"}).then((resp) { + var schema = (Map.from(resp)..remove("id")) .map((key, value) => MapEntry(key, value["type"]!)); MatchScoutQuestionSchema matchSchema = LinkedHashMap(); for (var MapEntry(key: columnname, value: sqltype) in schema.entries) { @@ -72,7 +74,7 @@ class SupabaseInterface { static final pitscoutStock = Stock>( fetcher: Fetcher.ofFuture((season) => Supabase.instance.client - .rpc('gettableschema', params: {"tablename": "${season}_pit"}).then((resp) => + .rpc('gettableschema', params: {"tablename": "pit_data_$season"}).then((resp) => (LinkedHashMap.from(resp) ..removeWhere((key, value) => {"event", "match", "team", "scouter"}.contains(key) || @@ -104,6 +106,60 @@ class SupabaseInterface { .then((data) => _achievements = data) : Future.value(_achievements); static void clearAchievements() => _achievements = null; + + static final aggregateStock = Stock>>( + sourceOfTruth: CachedSourceOfTruth(), + fetcher: Fetcher.ofFuture((key) async { + assert(key.event != null || key.team != null); + var response = LinkedHashMap.from(await Supabase.instance.client.functions + .invoke("match_aggregator_js", body: { + "season": key.season, + if (key.event != null) "event": key.event, + if (key.team != null) "team": key.team + }) + .then((resp) => + resp.status >= 400 ? throw Exception("HTTP Error ${resp.status}") : resp.data) + .catchError((e) => e is FunctionException && e.status == 404 ? {} : throw e)) + .map((k, v) => MapEntry(k, Map.from(v))); + if (key.event != null && key.team != null) { + return LinkedHashMap>.of( + response.map((k, v) => MapEntry(parseMatchInfo(k)!, Map.from(v[key.team])))); + // match: {scoretype: aggregated_count} + } + if (key.team != null) { + return LinkedHashMap.fromEntries(response.entries + .map((evententry) => Map.from(evententry.value).map( + (matchstring, matchscores) => MapEntry( + (event: evententry.key, match: parseMatchInfo(matchstring)!), + Map.from(matchscores)))) + .expand((e) => e.entries)); + } + throw UnimplementedError("No aggregate for that combination"); + })); + + static final distinctStock = Stock< + AggInfo, + ({ + Set scouters, + Set eventmatches, + Set events, + Set matches, + Set teams + })>( + sourceOfTruth: CachedSourceOfTruth(), + fetcher: Fetcher.ofFuture((key) { + var q = + Supabase.instance.client.from("match_scouting").select("*").eq("season", key.season); + if (key.event != null) q = q.eq("event", key.event!); + if (key.team != null) q = q.eq("team", key.team!); + return q.withConverter((data) => ( + scouters: data.map((e) => e["scouter"]).toSet(), + eventmatches: data.map((e) => e["event"] + e["match"]).toSet(), + events: data.map((e) => e["event"]).toSet(), + matches: data.map((e) => e["match"]).toSet(), + teams: data.map((e) => e["team"]).toSet() + )); + })); } typedef Achievement = ({ diff --git a/lib/pages/achievements.dart b/lib/pages/achievements.dart index 557b634..983abf3 100644 --- a/lib/pages/achievements.dart +++ b/lib/pages/achievements.dart @@ -37,14 +37,15 @@ class AchievementsPage extends StatelessWidget { @override Widget build(BuildContext context) => Column(children: [ AppBar(title: const Text("Achievements")), - Padding( - padding: const EdgeInsets.all(12), - child: TextField( - controller: _searchedText, - decoration: const InputDecoration( - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.search_rounded), - hintText: "Search"))), + if (MediaQuery.of(context).size.height > 400) + Padding( + padding: const EdgeInsets.all(12), + child: TextField( + controller: _searchedText, + decoration: const InputDecoration( + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.search_rounded), + hintText: "Search"))), Expanded( child: FutureBuilder( future: @@ -82,87 +83,78 @@ class AchievementsPage extends StatelessWidget { .contains(_searchedText.text.toLowerCase())) .toList(growable: false); return CarouselSlider( - items: data.map((achdata) { - var autoscrollcontroller = ScrollController(); - autoscrollcontroller.addListener(() => - autoscrollcontroller.offset >= - autoscrollcontroller.position.maxScrollExtent - ? autoscrollcontroller.jumpTo(0) - : autoscrollcontroller.position.pixels == 0 - ? autoscrollcontroller.animateTo( - autoscrollcontroller.position.maxScrollExtent, - duration: const Duration(seconds: 3), - curve: Curves.linear) - : null); - return Card( - clipBehavior: Clip.hardEdge, - child: Stack(fit: StackFit.expand, children: [ - if (achdata.approved != null) - ColoredBox( - color: { - AchievementApprovalStatus.approved: Colors.green, - AchievementApprovalStatus.rejected: Colors.red, - AchievementApprovalStatus.pending: Colors.grey - }[achdata.approved]! - .withAlpha(128)), - Padding( - padding: const EdgeInsets.all(18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row(mainAxisSize: MainAxisSize.max, children: [ - Flexible( - flex: 5, - fit: FlexFit.tight, - child: Align( - alignment: Alignment.centerLeft, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - achdata.achievement.name, - style: Theme.of(context) - .textTheme - .headlineMedium)))), - const Flexible( - flex: 1, - fit: FlexFit.tight, - child: SizedBox()), - Flexible( - flex: 2, - fit: FlexFit.tight, - child: Align( - alignment: Alignment.centerRight, - child: FittedBox( - child: Text( - "${achdata.achievement.points} pts", - style: Theme.of(context) - .textTheme - .headlineSmall! - .copyWith( - color: - Colors.grey))))) - ]), - const SizedBox(height: 12), - Expanded( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics( - parent: - NeverScrollableScrollPhysics()), - controller: autoscrollcontroller, - child: Text( - achdata.achievement.description, - style: Theme.of(context) - .textTheme - .bodyLarge))) - ])) - ])); - }).toList(growable: false), + items: data + .map((achdata) => Card( + clipBehavior: Clip.hardEdge, + child: Stack(fit: StackFit.expand, children: [ + if (achdata.approved != null) + ColoredBox( + color: { + AchievementApprovalStatus.approved: Colors.green, + AchievementApprovalStatus.rejected: Colors.red, + AchievementApprovalStatus.pending: Colors.grey + }[achdata.approved]! + .withAlpha(128)), + Padding( + padding: const EdgeInsets.all(18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + flex: 5, + fit: FlexFit.tight, + child: Align( + alignment: Alignment.centerLeft, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + achdata + .achievement.name, + style: Theme.of(context) + .textTheme + .headlineMedium)))), + const Flexible( + flex: 1, + fit: FlexFit.tight, + child: SizedBox()), + Flexible( + flex: 2, + fit: FlexFit.tight, + child: Align( + alignment: + Alignment.centerRight, + child: FittedBox( + child: Text( + "${achdata.achievement.points} pts", + style: Theme.of(context) + .textTheme + .headlineSmall! + .copyWith( + color: Colors + .grey))))) + ]), + const SizedBox(height: 12), + Expanded( + child: SingleChildScrollView( + physics: + const ClampingScrollPhysics(), + child: Text( + achdata.achievement.description, + style: Theme.of(context) + .textTheme + .bodyLarge))) + ])) + ]))) + .toList(growable: false), options: CarouselOptions( viewportFraction: max(300 / MediaQuery.of(context).size.width, 0.6), enlargeCenterPage: true, enlargeFactor: 0.2, - height: 200, + height: min(MediaQuery.of(context).size.height / 3, 200), enableInfiniteScroll: _searchedText.text.isEmpty, onPageChanged: (i, _) { if (_selectedAchievement.value.achievement.id == diff --git a/lib/pages/admin/achievementqueue.dart b/lib/pages/admin/achievementqueue.dart index 8ef7896..553b09b 100644 --- a/lib/pages/admin/achievementqueue.dart +++ b/lib/pages/admin/achievementqueue.dart @@ -67,133 +67,128 @@ class AchievementQueuePage extends StatelessWidget { ), ], body: SafeArea( - child: RefreshIndicator( - onRefresh: _fetch, - child: ListenableBuilder( - listenable: _items, - builder: (context, child) => _items.isEmpty - ? Center( - child: Text("No pending achievements for your team!", - style: Theme.of(context).textTheme.titleMedium)) - : ListView.builder( - primary: false, - physics: - const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), - itemCount: _items.length, - itemBuilder: (context, i) { - var e = _items[i]; - bool isDarkMode = Theme.of(context).brightness == Brightness.dark; - return ExpansionTile( - key: ObjectKey(e), - title: Text(e.achname), - subtitle: Text("${e.user} @ ${e.season}${e.event}"), - leading: e.image == null - ? null - : ClipRRect( - borderRadius: BorderRadius.circular(4), - clipBehavior: Clip.hardEdge, - child: GestureDetector( - child: Image( - image: e.image!, - isAntiAlias: false, - ), - onTap: () => showDialog( + child: ListenableBuilder( + listenable: _items, + builder: (context, child) => _items.isEmpty + ? Center( + child: Text("No pending achievements for your team!", + style: Theme.of(context).textTheme.titleMedium)) + : ListView.builder( + primary: false, + itemCount: _items.length, + itemBuilder: (context, i) { + var e = _items[i]; + bool isDarkMode = Theme.of(context).brightness == Brightness.dark; + return ExpansionTile( + key: ObjectKey(e), + title: Text(e.achname), + subtitle: Text("${e.user} @ ${e.season}${e.event}"), + leading: e.image == null + ? null + : ClipRRect( + borderRadius: BorderRadius.circular(4), + clipBehavior: Clip.hardEdge, + child: GestureDetector( + child: Image( + image: e.image!, + isAntiAlias: false, + ), + onTap: () => showDialog( + context: context, + builder: (context) => Dialog( + child: Image( + image: e.image!, + filterQuality: FilterQuality.medium))), + )), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + childrenPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + maintainState: true, + children: [ + ListTile( + leading: const Icon(Icons.checklist_rounded), + title: Text(e.achdesc), + subtitle: Text(e.achreqs), + dense: true), + ListTile( + leading: const Icon(Icons.person_search_rounded), + title: const Text("User Description"), + subtitle: Text(e.details, softWrap: true), + dense: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton.filledTonal( + onPressed: () => showDialog( context: context, - builder: (context) => Dialog( - child: Image( - image: e.image!, - filterQuality: FilterQuality.medium))), - )), - controlAffinity: ListTileControlAffinity.trailing, - expandedCrossAxisAlignment: CrossAxisAlignment.start, - childrenPadding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - maintainState: true, - children: [ - ListTile( - leading: const Icon(Icons.checklist_rounded), - title: Text(e.achdesc), - subtitle: Text(e.achreqs), - dense: true), - ListTile( - leading: const Icon(Icons.person_search_rounded), - title: const Text("User Description"), - subtitle: Text(e.details, softWrap: true), - dense: true, - trailing: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton.filledTonal( - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Reject Achievement?"), - actions: [ - OutlinedButton( - onPressed: () => - GoRouter.of(context).pop(), - child: const Text("Cancel")), - FilledButton( - onPressed: () => _update( - e.achid, - e.userid, - e.season, - e.event, - false) - .then((_) { - GoRouter.of(context).pop(); - _items.remove(e); - }).catchError((e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: Text( - e.toString()))); - }), - child: const Text("Confirm")) - ])), - icon: const Icon(Icons.close_rounded), - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll( - Colors.red[isDarkMode ? 700 : 300]))), - const SizedBox(width: 8), - IconButton.filledTonal( - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Approve Achievement?"), - actions: [ - OutlinedButton( - onPressed: () => - GoRouter.of(context).pop(), - child: const Text("Cancel")), - FilledButton( - onPressed: () => _update( - e.achid, - e.userid, - e.season, - e.event, - true) - .then((_) { - GoRouter.of(context).pop(); - _items.remove(e); - }).catchError((e) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: Text( - e.toString()))); - }), - child: const Text("Confirm")) - ])), - icon: const Icon(Icons.check_rounded), - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll( - Colors.green[isDarkMode ? 700 : 300])), - ) - ]), - ) - ]); - }))))); + builder: (context) => AlertDialog( + title: const Text("Reject Achievement?"), + actions: [ + OutlinedButton( + onPressed: () => + GoRouter.of(context).pop(), + child: const Text("Cancel")), + FilledButton( + onPressed: () => _update( + e.achid, + e.userid, + e.season, + e.event, + false) + .then((_) { + GoRouter.of(context).pop(); + _items.remove(e); + }).catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar( + content: + Text(e.toString()))); + }), + child: const Text("Confirm")) + ])), + icon: const Icon(Icons.close_rounded), + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + Colors.red[isDarkMode ? 700 : 300]))), + const SizedBox(width: 8), + IconButton.filledTonal( + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Approve Achievement?"), + actions: [ + OutlinedButton( + onPressed: () => + GoRouter.of(context).pop(), + child: const Text("Cancel")), + FilledButton( + onPressed: () => _update( + e.achid, + e.userid, + e.season, + e.event, + true) + .then((_) { + GoRouter.of(context).pop(); + _items.remove(e); + }).catchError((e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar( + content: + Text(e.toString()))); + }), + child: const Text("Confirm")) + ])), + icon: const Icon(Icons.check_rounded), + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + Colors.green[isDarkMode ? 700 : 300])), + ) + ]), + ) + ]); + })))); } class UniqueNotifyingList extends ChangeNotifier { diff --git a/lib/pages/admin/statgraph.dart b/lib/pages/admin/statgraph.dart index ad97d15..8dc56a0 100644 --- a/lib/pages/admin/statgraph.dart +++ b/lib/pages/admin/statgraph.dart @@ -17,8 +17,6 @@ import '../../utils.dart'; import '../metadata.dart'; import 'admin.dart'; -// FIXME cache supabase responses - class StatGraphPage extends StatelessWidget { final AnalysisInfo info = AnalysisInfo(); StatGraphPage({super.key}); @@ -65,9 +63,15 @@ class StatGraphPage extends StatelessWidget { ]))), appBar: AppBar( title: const Text("Statistic Graphs"), + actions: [ + IconButton( + icon: const Icon(Icons.refresh_rounded), + onPressed: SupabaseInterface.aggregateStock.clearAll) + ], bottom: TabBar( tabs: [ ValueTab( + // Season Selector SupabaseInterface.getAvailableSeasons().then((s) => s.toList()..add(0)), initialPositionCallback: (ss) { if (info.season == null) return ss.length - 1; @@ -76,16 +80,16 @@ class StatGraphPage extends StatelessWidget { }, onChange: (s) => info.season = s == 0 ? null : s), ListenableBuilder( + // Event Selector listenable: info.seasonNotifier, builder: (context, _) => ValueTab( info.season == null ? Future>.error({}) - : Supabase.instance.client - .from("${info.season}_match") - .select("event") - .withConverter( - (data) => data.map((e) => e["event"] as String).toSet()) - .then((e) => e.toList()..add("")), + : SupabaseInterface.distinctStock + .get((season: info.season!, event: null, team: info.team)).then( + (e) => e.events.toList() + ..sort() + ..add("")), initialPositionCallback: (ss) { if (info.event == null) return ss.length - 1; var i = ss.indexOf(info.event!); @@ -93,29 +97,26 @@ class StatGraphPage extends StatelessWidget { }, onChange: (e) => info.event = e.isEmpty ? null : e)), ListenableBuilder( + // Team Selector listenable: info.seasoneventNotifier, - builder: (context, _) { - Future> out; - if (info.season == null) { - out = Future.error({}); - } else { - var query = Supabase.instance.client - .from("${info.season}_match") - .select("team"); - if (info.event != null) query = query.eq("event", info.event!); - out = query - .withConverter( - (data) => data.map((e) => e["team"].toString()).toSet()) - .then((t) => t.toList()..add("")); - } - return ValueTab(out, - initialPositionCallback: (ss) { - if (info.team == null) return ss.length - 1; - var i = ss.indexOf(info.team!); - return i >= 0 ? i : ss.length - 1; - }, - onChange: (t) => info.team = t.isEmpty ? null : t); - }) + builder: (context, _) => ValueTab( + info.season == null + ? Future.error({}) + : SupabaseInterface.distinctStock + .get(( + season: info.season!, + event: info.event, + team: null + )).then((t) => t.teams.toList() + ..sort( + (a, b) => (int.tryParse(a) ?? 0) - (int.tryParse(b) ?? 0)) + ..add("")), + initialPositionCallback: (ss) { + if (info.team == null) return ss.length - 1; + var i = ss.indexOf(info.team!); + return i >= 0 ? i : ss.length - 1; + }, + onChange: (t) => info.team = t.isEmpty ? null : t)) ], padding: const EdgeInsets.symmetric(horizontal: 12), dividerHeight: 0, @@ -129,47 +130,34 @@ class StatGraphPage extends StatelessWidget { child: Container( padding: const EdgeInsets.only(left: 12, right: 12, bottom: 12), alignment: Alignment.topCenter, - // FIXME https://supabase.com/blog/postgrest-12#aggregate-functions - child: TabBarView( - children: [ + constraints: const BoxConstraints(maxHeight: 60), + child: TabBarView(children: [ ListenableBuilder( listenable: info.seasonNotifier, builder: (context, _) => ValueDetailTabView(info.season == null ? Future.value(["No Season Selected"]) - : Supabase.instance.client - .from("${info.season!}_match") - .select("scouter, event, match, team") - .withConverter((resp) => ( - scouters: resp.map((e) => e["scouter"]).toSet().length, - matches: - resp.map((e) => e["event"] + e["match"]).toSet().length, - teams: resp.map((e) => e["team"]).toSet().length - )) - .then((data) => [ - "${data.scouters} Scouter${data.scouters != 1 ? 's' : ''}", - "${data.matches} Match${data.matches != 1 ? 'es' : ''}", - "${data.teams} Team${data.teams != 1 ? 's' : ''}" - ]))), + : SupabaseInterface.distinctStock + .get((season: info.season!, event: null, team: null)).then( + (data) => [ + "${data.scouters.length} Scouter${data.scouters.length != 1 ? 's' : ''}", + "${data.eventmatches.length} Match${data.eventmatches.length != 1 ? 'es' : ''}", + "${data.teams.length} Team${data.teams.length != 1 ? 's' : ''}" + ]))), ListenableBuilder( listenable: info.seasoneventNotifier, builder: (context, _) => ValueDetailTabView(info.event == null ? Future.value(["No Event Selected"]) : info.season == null ? Future.value(["No Season Selected"]) - : Supabase.instance.client - .from("${info.season!}_match") - .select("scouter, match, team") - .eq("event", info.event!) - .withConverter((resp) => ( - scouters: resp.map((e) => e["scouter"]).toSet().length, - matches: resp.map((e) => e["match"]).toSet().length, - teams: resp.map((e) => e["team"]).toSet().length - )) - .then((data) => [ - "${data.scouters} Scouter${data.scouters != 1 ? 's' : ''}", - "${data.matches} Match${data.matches != 1 ? 'es' : ''}", - "${data.teams} Team${data.teams != 1 ? 's' : ''}" - ]))), + : SupabaseInterface.distinctStock.get(( + season: info.season!, + event: info.event!, + team: null + )).then((data) => [ + "${data.scouters.length} Scouter${data.scouters.length != 1 ? 's' : ''}", + "${data.matches.length} Match${data.matches.length != 1 ? 'es' : ''}", + "${data.teams.length} Team${data.teams.length != 1 ? 's' : ''}" + ]))), ListenableBuilder( listenable: info, builder: (context, _) { @@ -179,16 +167,14 @@ class StatGraphPage extends StatelessWidget { } else if (info.season == null) { out = Future.value(["No Season Selected"]); } else { - var query = Supabase.instance.client - .from("${info.season!}_match") - .select("match") - .eq("team", info.team!); - if (info.event != null) query = query.eq("event", info.event!); out = Future.wait(>>[ - query - .withConverter( - (data) => data.map((e) => e["match"]).toSet().length) - .then((data) => ["$data Match${data != 1 ? 'es' : ''}"]), + SupabaseInterface.distinctStock.get(( + season: info.season!, + event: info.event, + team: info.team + )).then((data) => [ + "${data.matches.length} Match${data.matches.length != 1 ? 'es' : ''}" + ]), if (info.event != null && info.team != null) BlueAlliance.getOPR(info.season!, info.event!, info.team!) .then>((data) => data != null @@ -203,17 +189,9 @@ class StatGraphPage extends StatelessWidget { } return ValueDetailTabView(out); }) - ] - .map((e) => DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: const ToplessHitchedBorder( - BorderSide(width: 3.5, color: Colors.white))), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), child: e))) - .toList()))), + ]))), Flexible( - flex: 3, + flex: 4, fit: FlexFit.tight, child: SafeArea( minimum: const EdgeInsets.all(24), @@ -277,86 +255,94 @@ class ValueTab extends StatelessWidget { LinkedHashSet.of(snapshot.data!.map((e) => e.toString())); return ListenableBuilder( listenable: _searchMode, - builder: (context, _) => - IndexedStack(index: _searchMode.value ? 0 : 1, children: [ - Autocomplete( - optionsBuilder: (input) => snapshot.data! - .where((e) => e.toString().startsWith(input.toString())), - fieldViewBuilder: (context, textEditingController, focusNode, - onFieldSubmitted) => - TextFormField( - spellCheckConfiguration: - const SpellCheckConfiguration.disabled(), - autocorrect: false, - enableSuggestions: false, - textCapitalization: TextCapitalization.none, - keyboardType: T == int || T == double - ? TextInputType.numberWithOptions( - decimal: T == double) - : TextInputType.text, - inputFormatters: T == int - ? [FilteringTextInputFormatter.digitsOnly] - : T == double - ? [ - FilteringTextInputFormatter.allow( - RegExp(r"[0-9.]")) - ] - : [], - controller: textEditingController..clear(), - focusNode: focusNode..canRequestFocus = true, - onFieldSubmitted: (value) { - if (!stringifiedOptions.contains(value)) return; - onFieldSubmitted(); - var v = stringifiedOptions.toList().indexOf(value); - _carouselController.jumpToPage(v); - if (onChange != null) onChange!(snapshot.data![v]); - _searchMode.value = false; - }, - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) => - stringifiedOptions.contains(value) + builder: (context, _) => IndexedStack( + index: _searchMode.value ? 0 : 1, + sizing: StackFit.expand, + children: [ + Autocomplete( + optionsBuilder: (input) => snapshot.data!.where( + (e) => e.toString().startsWith(input.toString())), + fieldViewBuilder: (context, textEditingController, + focusNode, onFieldSubmitted) => + TextFormField( + spellCheckConfiguration: + const SpellCheckConfiguration.disabled(), + autocorrect: false, + enableSuggestions: false, + textCapitalization: TextCapitalization.none, + keyboardType: T == int || T == double + ? TextInputType.numberWithOptions( + decimal: T == double) + : TextInputType.text, + inputFormatters: [ + if (T == int) + FilteringTextInputFormatter.digitsOnly, + if (T == double) + FilteringTextInputFormatter.allow( + RegExp(r"[0-9.]")) + ], + decoration: const InputDecoration( + border: InputBorder.none), + controller: textEditingController..clear(), + focusNode: focusNode, + onFieldSubmitted: (value) { + if (!stringifiedOptions.contains(value)) return; + onFieldSubmitted(); + var v = + stringifiedOptions.toList().indexOf(value); + _carouselController.jumpToPage(v); + if (onChange != null) { + onChange!(snapshot.data![v]); + } + _searchMode.value = false; + }, + autovalidateMode: + AutovalidateMode.onUserInteraction, + validator: (value) => + stringifiedOptions.contains(value) + ? null + : "Invalid", + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.center), + onSelected: (value) { + if (onChange != null) onChange!(value); + _carouselController + .jumpToPage(snapshot.data!.indexOf(value)); + _searchMode.value = false; + }), + CarouselSlider( + carouselController: _carouselController, + items: snapshot.data! + .map((e) => Text( + e == 0.0 ? "" : e.toString(), + overflow: kIsWeb + ? TextOverflow.ellipsis + : TextOverflow.fade, + softWrap: false, + maxLines: 1, + textHeightBehavior: const TextHeightBehavior( + leadingDistribution: + TextLeadingDistribution.even), + style: Theme.of(context) + .textTheme + .headlineSmall! + .copyWith(fontSize: 22), + )) + .toList(), + options: CarouselOptions( + height: 40, + viewportFraction: 1, + scrollDirection: Axis.horizontal, + initialPage: initialPositionCallback == + null // FIXME this does not work because carousel is stupid as balls + ? snapshot.data!.length - 1 + : initialPositionCallback!(snapshot.data!), + enableInfiniteScroll: snapshot.data!.length > 3, + onPageChanged: (i, _) => + onChange == null || snapshot.data!.length <= i ? null - : "Invalid", - textAlign: TextAlign.center, - textAlignVertical: TextAlignVertical.center), - onSelected: (value) { - if (onChange != null) onChange!(value); - _carouselController - .jumpToPage(snapshot.data!.indexOf(value)); - _searchMode.value = false; - }), - CarouselSlider( - carouselController: _carouselController, - items: snapshot.data! - .map((e) => Text( - e == 0.0 ? "" : e.toString(), - overflow: kIsWeb - ? TextOverflow.ellipsis - : TextOverflow.fade, - softWrap: false, - maxLines: 1, - textHeightBehavior: const TextHeightBehavior( - leadingDistribution: - TextLeadingDistribution.even), - style: Theme.of(context) - .textTheme - .headlineSmall! - .copyWith(fontSize: 22), - )) - .toList(), - options: CarouselOptions( - height: 40, - viewportFraction: 1, - scrollDirection: Axis.horizontal, - initialPage: initialPositionCallback == null - ? snapshot.data!.length - 1 - : initialPositionCallback!(snapshot.data!), - enableInfiniteScroll: snapshot.data!.length > 3, - onPageChanged: (i, _) => - onChange == null || snapshot.data!.length <= i - ? null - : onChange!(snapshot.data![i]))) - ])); + : onChange!(snapshot.data![i]))) + ])); }))))); } @@ -367,28 +353,21 @@ class ValueDetailTabView extends StatelessWidget { @override Widget build(BuildContext context) => FutureBuilder( future: Future.value(detail), - builder: (context, snapshot) => !snapshot.hasData - ? const Center(child: CircularProgressIndicator()) - : LayoutBuilder( - builder: (context, constraints) => constraints.maxWidth > 3 * constraints.maxHeight - ? FittedBox( + builder: (context, snapshot) => DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: const ToplessHitchedBorder(BorderSide(width: 3.5, color: Colors.white))), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: !snapshot.hasData + ? const Center(child: CircularProgressIndicator()) + : FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.center, child: Text(snapshot.data!.join(", "), textAlign: TextAlign.center, style: Theme.of(context).textTheme.labelLarge, - textScaler: const TextScaler.linear(1.5))) - : Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: snapshot.data! - .map((e) => Text( - e, - style: Theme.of(context).textTheme.labelLarge, - textScaler: const TextScaler.linear(1.3), - )) - .toList()))); + textScaler: const TextScaler.linear(1.5)))))); } class AnalysisInfo extends ChangeNotifier { @@ -430,20 +409,7 @@ class TeamAtEventGraph extends StatelessWidget { @override Widget build(BuildContext context) => FutureBuilder( future: Future.wait([ - Supabase.instance.client.functions - .invoke( - "match_aggregator_js?season=$season&event=$event", // workaround for lack of query params - ) - .then((resp) => resp.status >= 400 - ? throw Exception("HTTP Error ${resp.status}") - : (Map.from(resp.data) // match: - .map((key, value) => - MapEntry(key, Map.from(value))) // team: - ..removeWhere((key, value) => !value.containsKey(team))) - .map((key, value) => MapEntry(parseMatchInfo(key)!, - Map.from(value[team]))) // match: {scoretype: aggregated_count} - .entries - .toSet()), + SupabaseInterface.aggregateStock.get((season: season, event: event, team: team)), BlueAlliance.stock .get((season: season, event: event, match: null)) .then((eventMatches) => Future.wait(eventMatches.keys.map((match) => BlueAlliance.stock @@ -455,7 +421,7 @@ class TeamAtEventGraph extends StatelessWidget { .toList() ..sort((a, b) => compareMatchInfo(b, a))) ]).then((results) => ( - data: results[0] as Set>>, + data: results[0] as LinkedHashMap>, ordinalMatches: results[1] as List )), builder: (context, snapshot) => snapshot.hasError @@ -465,7 +431,7 @@ class TeamAtEventGraph extends StatelessWidget { children: [ Icon(Icons.warning_rounded, color: Colors.red[700], size: 50), const SizedBox(height: 20), - Text(snapshot.error.toString()) + Text(snapshot.error.runtimeType.toString()) ]) : LineChart( LineChartData( @@ -520,7 +486,7 @@ class TeamAtEventGraph extends StatelessWidget { radius: 5, strokeColor: Colors.transparent, strokeWidth: 0)), - spots: snapshot.data!.data + spots: snapshot.data!.data.entries .map((e) => FlSpot( snapshot.data!.ordinalMatches.indexOf(e.key).toDouble(), scoreTotal(e.value, season: season, period: period) @@ -537,7 +503,7 @@ class TeamAtEventGraph extends StatelessWidget { ? [] : [ HorizontalLine( - y: snapshot.data!.data + y: snapshot.data!.data.entries .map((e) => scoreTotal(e.value, season: season)) .followedBy([0]).reduce((v, e) => v + e) / snapshot.data!.data.length.toDouble(), @@ -554,21 +520,8 @@ class TeamInSeasonGraph extends StatelessWidget { @override Widget build(BuildContext context) => FutureBuilder( - future: Supabase.instance.client.functions - .invoke( - "match_aggregator_js?season=$season&team=$team", // workaround for lack of query params - ) - .then((resp) => resp.status >= 400 - ? throw Exception("HTTP Error ${resp.status}") - : Map.fromEntries(Map.from(resp.data) - .entries - .map((evententry) => Map.from(evententry.value) // event: - .map((matchstring, matchscores) => MapEntry( - // match: - (event: evententry.key, match: parseMatchInfo(matchstring)!), - Map.from(matchscores)))) - .expand((e) => e.entries))) - .then((result) { + future: SupabaseInterface.aggregateStock + .get((season: season, event: null, team: team)).then((result) { Map op = {}; Map ot = {}; Map om = {}; @@ -592,101 +545,6 @@ class TeamInSeasonGraph extends StatelessWidget { misc: om.map((key, value) => MapEntry(key, value / result.values.length.toDouble())) ); }), - builder: (context, snapshot) => Flex( - direction: MediaQuery.of(context).size.width > 600 ? Axis.horizontal : Axis.vertical, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - PieChart( - PieChartData( - centerSpaceRadius: 100, - sections: !snapshot.hasData - ? [] - : snapshot.data!.period.entries - .map((e) => PieChartSectionData( - value: e.value, - title: e.key.name, - color: e.key.graphColor, - showTitle: true, - titleStyle: Theme.of(context).textTheme.labelSmall, - borderSide: - BorderSide(color: Theme.of(context).colorScheme.outline))) - .toList()), - swapAnimationDuration: Durations.extralong3, - swapAnimationCurve: Curves.easeInSine), - PieChart( - PieChartData( - centerSpaceRadius: 100, - sections: !snapshot.hasData - ? [] - : snapshot.data!.type.entries - .map((e) => PieChartSectionData( - value: e.value, - title: e.key, - color: Theme.of(context).colorScheme.primaryContainer, - showTitle: true, - titleStyle: Theme.of(context).textTheme.labelSmall, - borderSide: - BorderSide(color: Theme.of(context).colorScheme.outline))) - .toList()), - swapAnimationDuration: Durations.extralong3, - swapAnimationCurve: Curves.easeInSine) - ] - .map((e) => Flexible( - child: ConstrainedBox( - constraints: BoxConstraints.tight(const Size.square(400)), - child: Transform.scale( - scale: min(MediaQuery.of(context).size.shortestSide / 400, 1), - child: e)))) - .followedBy([ - if (season == 2024 && snapshot.hasData) - Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: { - if (snapshot.data!.misc.containsKey('comments_agility')) - "Agility": snapshot.data!.misc['comments_agility']!, - if (snapshot.data!.misc.containsKey('comments_contribution')) - "Contribution": snapshot.data!.misc['comments_contribution']! - } - .entries - .map((e) => Card.filled( - color: Theme.of(context).colorScheme.secondaryContainer, - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 70), - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text(e.key), - Text("${(e.value * 5).toStringAsFixed(1)} / 5") - ]))))) - .toList()) - ]).toList())); -} - -class EventInSeasonRankings extends StatelessWidget { - final int season; - final String event; - const EventInSeasonRankings({super.key, required this.season, required this.event}); - - @override - Widget build(BuildContext context) => FutureBuilder( - future: Supabase.instance.client.functions - .invoke("event_aggregator?season=$season&event=$event") - .then((resp) => resp.status >= 400 - ? throw Exception("HTTP Error ${resp.status}") - : LinkedHashMap.fromEntries(Map.from(resp.data) - .entries - .whereType>() - .toList() - ..sort((a, b) => a.value == b.value - ? 0 - : a.value > b.value - ? -1 - : 1))), builder: (context, snapshot) => snapshot.hasError ? Column( mainAxisAlignment: MainAxisAlignment.center, @@ -694,13 +552,129 @@ class EventInSeasonRankings extends StatelessWidget { children: [ Icon(Icons.warning_rounded, color: Colors.red[700], size: 50), const SizedBox(height: 20), - Text(snapshot.error.toString()) + Text(snapshot.error.runtimeType.toString()) ]) - : !snapshot.hasData - ? const Center(child: CircularProgressIndicator()) - : ListView( - children: snapshot.data!.entries - .map((e) => - ListTile(title: Text(e.key), trailing: Text(e.value.toStringAsFixed(2)))) - .toList())); + : Flex( + direction: MediaQuery.of(context).size.width > 600 ? Axis.horizontal : Axis.vertical, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + PieChart( + PieChartData( + centerSpaceRadius: 100, + sections: !snapshot.hasData + ? [] + : snapshot.data!.period.entries + .where((e) => e.key != GamePeriod.others && e.value > 0) + .map((e) => PieChartSectionData( + value: e.value, + title: e.key.name, + color: e.key.graphColor, + showTitle: true, + titleStyle: Theme.of(context).textTheme.labelSmall, + borderSide: + BorderSide(color: Theme.of(context).colorScheme.outline))) + .toList()), + swapAnimationDuration: Durations.extralong3, + swapAnimationCurve: Curves.easeInSine), + PieChart( + PieChartData( + centerSpaceRadius: 100, + sections: !snapshot.hasData + ? [] + : snapshot.data!.type.entries + .map((e) => PieChartSectionData( + value: e.value, + title: e.key, + color: Theme.of(context).colorScheme.primaryContainer, + showTitle: true, + titleStyle: Theme.of(context).textTheme.labelSmall, + borderSide: + BorderSide(color: Theme.of(context).colorScheme.outline))) + .toList()), + swapAnimationDuration: Durations.extralong3, + swapAnimationCurve: Curves.easeInSine) + ] + .map((e) => Flexible( + child: ConstrainedBox( + constraints: BoxConstraints.tight(const Size.square(400)), + child: Transform.scale( + scale: min(MediaQuery.of(context).size.shortestSide / 400, 1), + child: e)))) + .followedBy([ + if (season == 2024 && snapshot.hasData) + Flex( + direction: + MediaQuery.of(context).size.width > 600 ? Axis.vertical : Axis.horizontal, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: { + if (snapshot.data!.misc.containsKey('comments_agility')) + "Agility": snapshot.data!.misc['comments_agility']!, + if (snapshot.data!.misc.containsKey('comments_contribution')) + "Contribution": snapshot.data!.misc['comments_contribution']! + } + .entries + .map((e) => Card.filled( + color: Theme.of(context).colorScheme.secondaryContainer, + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 80, maxHeight: 60), + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text(e.key), + Text("${(e.value * 5).toStringAsFixed(1)} / 5") + ]))))) + .toList()) + ]).toList())); +} + +class EventInSeasonRankings extends StatelessWidget { + final int season; + final String event; + EventInSeasonRankings({super.key, required this.season, required this.event}); + + final ValueNotifier searchTerm = ValueNotifier(""); + @override + Widget build(BuildContext context) => CustomScrollView(slivers: [ + // SliverAppBar(primary: false, automaticallyImplyLeading: false, title: TextField()), + FutureBuilder( + future: Supabase.instance.client.functions + .invoke("event_aggregator?season=$season&event=$event") + .then((resp) => resp.status >= 400 + ? throw Exception("HTTP Error ${resp.status}") + : LinkedHashMap.fromEntries(Map.from(resp.data) + .entries + .whereType>() + .toList() + ..sort((a, b) => a.value == b.value + ? 0 + : a.value > b.value + ? -1 + : 1))), + builder: (context, snapshot) { + if (snapshot.hasError) { + return SliverFillRemaining( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.warning_rounded, color: Colors.red[700], size: 50), + const SizedBox(height: 20), + Text(snapshot.error.toString()) + ])); + } + if (!snapshot.hasData) { + return const SliverFillRemaining( + hasScrollBody: false, child: Center(child: CircularProgressIndicator())); + } + return SliverAnimatedInList(snapshot.data!.entries.toList(), + builder: (context, e) => + ListTile(title: Text(e.key), trailing: Text(e.value.toStringAsFixed(2)))); + }) + ]); } diff --git a/lib/pages/matchscout.dart b/lib/pages/matchscout.dart index 37655e0..4ed6df9 100644 --- a/lib/pages/matchscout.dart +++ b/lib/pages/matchscout.dart @@ -25,10 +25,20 @@ enum MatchScoutQuestionTypes { .firstWhere((type) => type.sqlType == s, orElse: () => MatchScoutQuestionTypes.error); } -Future submitInfo(Map data, {int? season}) async => await SupabaseInterface - .canConnect - ? Supabase.instance.client.from("${season ?? Configuration.instance.season}_match").insert(data) - : LocalStoreInterface.addMatch(season ?? Configuration.instance.season, data); +typedef MatchScoutInfoSerialized = ({int season, String event, String match, String team}); +Future submitInfo(MatchScoutInfoSerialized key, + {required Map fields}) async => + await SupabaseInterface.canConnect + ? Supabase.instance.client + .from("match_scouting") + .insert( + {"season": key.season, "event": key.event, "match": key.match, "team": key.team}) + .select("id") + .single() + .then((r) => Supabase.instance.client + .from("match_data_${key.season}") + .insert(fields..["id"] = r["id"])) + : LocalStoreInterface.addMatch(key, fields); class MatchScoutPage extends StatefulWidget { const MatchScoutPage({super.key}); @@ -192,12 +202,13 @@ class _MatchScoutPageState extends State with WidgetsBindingObse _fields.clear(); _formKey.currentState!.save(); MatchInfo currmatch = info.match!; - submitInfo({ - ..._fields, - "event": Configuration.event, - "match": stringifyMatchInfo(currmatch), - "team": info.team - }).then((_) async { + submitInfo(( + season: Configuration.instance.season, + event: Configuration.event!, + match: stringifyMatchInfo(currmatch), + team: info.team! + ), fields: _fields) + .then((_) async { _formKey.currentState!.reset(); if (currmatch.level == MatchLevel.qualification && currmatch.index < info.highestQual) { diff --git a/lib/pages/pitscout.dart b/lib/pages/pitscout.dart index 15c4a66..f39196d 100644 --- a/lib/pages/pitscout.dart +++ b/lib/pages/pitscout.dart @@ -10,7 +10,7 @@ import '../interfaces/localstore.dart'; import '../interfaces/supabase.dart'; Future?> _getPrevious(int team) => Supabase.instance.client - .from("${Configuration.instance.season}_pit") + .from("pit_data_${Configuration.instance.season}") .select() .eq("event", Configuration.event!) .eq("team", team) @@ -20,10 +20,12 @@ Future?> _getPrevious(int team) => Supabase.instance.client ? {} : Map.castFrom(value..removeWhere((k, _) => {"event", "team"}.contains(k)))); -Future submitInfo(Map data, {int? season}) async => (await SupabaseInterface - .canConnect) - ? Supabase.instance.client.from("${season ?? Configuration.instance.season}_pit").upsert(data) - : LocalStoreInterface.addPit(season ?? Configuration.instance.season, data); +Future submitInfo(Map data, {int? season}) async => + (await SupabaseInterface.canConnect) + ? Supabase.instance.client + .from("pit_data_${season ?? Configuration.instance.season}") + .upsert(data) + : LocalStoreInterface.addPit(season ?? Configuration.instance.season, data); class PitScoutPage extends StatefulWidget { const PitScoutPage({super.key}); @@ -47,7 +49,7 @@ class _PitScoutPageState extends State { .then((data) => Set.of(data.keys.map(int.parse))) .then((teams) async { Set filledteams = await Supabase.instance.client - .from("${Configuration.instance.season}_pit") + .from("pit_data_${Configuration.instance.season}") .select("team") .eq("event", Configuration.event!) .withConverter((value) => value.map((e) => e['team']).toSet()) diff --git a/lib/pages/savedresponses.dart b/lib/pages/savedresponses.dart index 54dbd3e..3067051 100644 --- a/lib/pages/savedresponses.dart +++ b/lib/pages/savedresponses.dart @@ -135,7 +135,12 @@ class _RespList extends StatelessWidget { dataList.remove(id, dontUpdate: true), handler(true), (id.startsWith("match") - ? matchscout.submitInfo(data, season: data.remove('season')) + ? matchscout.submitInfo(( + season: data.remove('season') as int, + event: data.remove('event') as String, + match: data.remove('match') as String, + team: data.remove('team') as String + ), fields: data) : id.startsWith("pit") ? pitscout.submitInfo(data, season: data.remove('season')) : throw Exception("Invalid LocalStore ID: $id")) diff --git a/lib/utils.dart b/lib/utils.dart index eac130e..4285a5a 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -462,3 +462,36 @@ class ToplessHitchedBorder extends BoxBorder { return '${objectRuntimeType(this, 'ToplessHitchedBorder')}($bottom)'; } } + +class SliverAnimatedInList extends StatefulWidget { + final List list; + final Widget Function(BuildContext, T) builder; + const SliverAnimatedInList(this.list, {required this.builder, super.key}); + + @override + State createState() => _SliverAnimatedInListState(); +} + +class _SliverAnimatedInListState extends State { + final GlobalKey _animKey = GlobalKey(); + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) => Future.forEach( + widget.list.indexed, + (e) => Future.delayed(Durations.medium1, () { + setState(() { + _animKey.currentState!.insertItem(e.$1, duration: Durations.medium3); + }); + }))); + super.initState(); + } + + @override + Widget build(BuildContext context) => SliverAnimatedList( + key: _animKey, + itemBuilder: (context, i, anim) => AnimatedSlide( + offset: Offset(0, anim.value * -i.toDouble()), + duration: Durations.short1, + child: widget.builder(context, widget.list[i]))); +} diff --git a/pubspec.lock b/pubspec.lock index 5972d0a..6999fd6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,10 +106,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: cb44f7831b23a6bdd0f501718b0d2e8045cbc625a15f668af37ddb80314821db + sha256: "87e11b9df25a42e2db315b8b7a51fae8e66f57a4b2f50ec4b822d0fa155e6b52" url: "https://pub.dev" source: hosted - version: "0.6.21" + version: "0.6.22" flutter_svg: dependency: "direct main" description: @@ -143,10 +143,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" + sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.2.1" gotrue: dependency: transitive description: diff --git a/supabase/functions/event_aggregator/index.ts b/supabase/functions/event_aggregator/index.ts index 6844dd0..9000077 100644 --- a/supabase/functions/event_aggregator/index.ts +++ b/supabase/functions/event_aggregator/index.ts @@ -35,7 +35,8 @@ Deno.serve(async (req: Request) => { ); } // Database Fetching - const { data, error } = await supabase.from(`${params.get("season")}_match`).select().eq("event", params.get("event")); + const { data, error } = await supabase.from(`match_data_${params.get("season")}`) + .select("*, match_scouting!inner(event, team)").eq("match_scouting.event", params.get("event")); if (!data || data.length === 0) { return new Response( `No Data Found for ${params.get("season")}${params.has("event") ? params.get("event") : ""}\n${error?.message}`, @@ -46,14 +47,14 @@ Deno.serve(async (req: Request) => { ); } - let agg: {[key: string]: Set} = {}; + const agg: {[key: string]: Set} = {}; for (const entry of data) { const skill = entry["comments_agility"] / ( ((5 * entry["comments_fouls"] + 1) * (entry["comments_defensive"] ? 0.7 : 1)) ); - if (!agg[entry["team"]]) agg[entry["team"]] = new Set(); - agg[entry["team"]].add(skill); + if (!agg[entry.match_scouting["team"]]) agg[entry.match_scouting["team"]] = new Set(); + agg[entry.match_scouting["team"]].add(skill); } return Response.json(Object.fromEntries( - Object.entries(agg).map(([k, v]: [string, Set]) => [k, v.values().reduce((a, b) => a+b) / v.size]) + Object.entries(agg).map(([k, v]: [string, Set]) => [k, [...v.values()].reduce((a, b) => a+b) / v.size]) ), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } diff --git a/supabase/functions/match_aggregator_js/index.ts b/supabase/functions/match_aggregator_js/index.ts index 3f2722a..ca98d4b 100644 --- a/supabase/functions/match_aggregator_js/index.ts +++ b/supabase/functions/match_aggregator_js/index.ts @@ -27,7 +27,10 @@ Deno.serve(async (req: Request) => { // Request Argument Validation const params: URLSearchParams = new URL(req.url).searchParams; - // for (const entry of await req.json()) params.append(entry[0], entry[1]); + for (const entry of await req.json() + .then((p) => Object.entries(p)) + .catch((e) => {console.warn(e); return []})) + params.append(entry[0], entry[1]); if (!params.has("season") || !(params.has("event") || params.has("team"))) { return new Response( "Missing Required Parameters\nseason: valid frc season year (e.g. 2023)\n\nevent: valid tba event code (e.g. casf)\nOR\nteam: valid frc team number (e.g. 4159)", @@ -38,9 +41,9 @@ Deno.serve(async (req: Request) => { ); } // Database Fetching - let query = supabase.from(`${params.get("season")}_match`).select(); - if (params.has("event")) query = query.eq("event", params.get("event")!) - if (params.has("team")) query = query.eq("team", params.get("team")!) + let query = supabase.from(`match_data_${params.get("season")}`).select("*, match_scouting!inner(scouter, event, match, team)"); + if (params.has("event")) query = query.eq("match_scouting.event", params.get("event")!) + if (params.has("team")) query = query.eq("match_scouting.team", params.get("team")!) const { data, error } = await query; if (!data || data.length === 0) { return new Response( @@ -57,14 +60,13 @@ Deno.serve(async (req: Request) => { [key: string]: { [key: string]: { [key: string]: string } }; } = {}; // {team / event: {match: {question: value}}} for (const scoutingEntry of data) { - const mkey: string = !params.has("event") ? scoutingEntry["event"] : scoutingEntry["team"]; + const mkey: string = !params.has("event") ? scoutingEntry.match_scouting["event"] : scoutingEntry.match_scouting["team"]; if (agg[mkey] == null) agg[mkey] = {}; - const match: string = scoutingEntry["match"]; + const match: string = scoutingEntry.match_scouting["match"]; if (agg[mkey][match] == null) agg[mkey][match] = {}; - const scouter: string | undefined = scoutingEntry["scouter"]; - ["event", "match", "team", "scouter"].forEach((k) => - delete scoutingEntry[k] - ); + const scouter: string | undefined = scoutingEntry.match_scouting["scouter"]; + delete scoutingEntry.match_scouting; + delete scoutingEntry.id; for (const [key, value] of Object.entries(scoutingEntry)) { if (!(typeof value === "string") || value.length === 0) continue; if (agg[mkey][match][key] == null) agg[mkey][match][key] = ""; @@ -93,15 +95,14 @@ Deno.serve(async (req: Request) => { [key: string]: { [key: string]: { [key: string]: Set } }; } = {}; // {match: {team: {scoretype: value}}} for (const scoutingEntry of data) { - const match: string = scoutingEntry["match"]; + const match: string = scoutingEntry.match_scouting["match"]; if (agg[match] == null) agg[match] = {}; - const team: string = scoutingEntry["team"]; + const team: string = scoutingEntry.match_scouting["team"]; if (agg[match][team] == null) agg[match][team] = {}; if (!(match in tbadata)) continue; - ["event", "match", "team", "scouter"].forEach((k) => - delete scoutingEntry[k] - ); + delete scoutingEntry.match_scouting; + delete scoutingEntry.id; for (let [key, value] of Object.entries(scoutingEntry)) { if (typeof value === "boolean") value = value ? 1 : 0; if (typeof value !== "number") continue; @@ -166,14 +167,13 @@ Deno.serve(async (req: Request) => { } = {}; // {event: {match: {scoretype: [values]}}} const team = params.get("team")!; for (const scoutingEntry of data) { - const event: string = scoutingEntry["event"]; + const event: string = scoutingEntry.match_scouting["event"]; if (agg[event] == null) agg[event] = {}; - const match: string = scoutingEntry["match"]; + const match: string = scoutingEntry.match_scouting["match"]; if (agg[event][match] == null) agg[event][match] = {}; - ["event", "match", "team", "scouter"].forEach((k) => - delete scoutingEntry[k] - ); + delete scoutingEntry.match_scouting; + delete scoutingEntry.id; for (let [key, value] of Object.entries(scoutingEntry)) { if (typeof value === "boolean") value = value ? 1 : 0; if (typeof value !== "number") continue;