Skip to content

Custom fields management and analysis - #130

Open
MangoSwirl wants to merge 15 commits into
masterfrom
custom-fields
Open

Custom fields management and analysis#130
MangoSwirl wants to merge 15 commits into
masterfrom
custom-fields

Conversation

@MangoSwirl

@MangoSwirl MangoSwirl commented Aug 2, 2026

Copy link
Copy Markdown
Member

Adds custom fields to the dashboard: scouting leads configure custom post-match questions, and their answers appear throughout analysis clearly marked as custom.

What's here

  • New "Custom Fields" page off the nav drawer (scouting leads only): list, create/edit with an options editor, drag-to-reorder, archive/unarchive. Type is immutable after creation (matching the server contract).
  • custom_fields.dart API extension + in-memory definitions cache.
  • Raw report: an "Asked by your team" section rendering each answer by type.
  • Team Lookup: number answers as an "Asked by your team" category with per-match detail graphs; select answers as breakdown distributions.
  • Picklists: number fields become sliders, resilient to archived/unknown fields; custom weights sent via a single customWeights param and persisted on shared picklists.
  • CustomFieldIndicator — a shared "Custom" chip on picklist/breakdown surfaces; grouping headers mark the rest. Never two markers on one surface.

Every custom section is hidden when empty (no blank scaffolding).

Testing

Validated live on an iOS simulator against a local server: created a field through the real UI, and confirmed the category average, detail sparkline, and breakdown distributions all render with the correct seeded values, plus cross-team isolation (a 9470 viewer sees none of 8033's custom data). flutter analyze clean (no new issues).


Related PRs — this feature spans four repos:

Scouting leads can configure custom post-match questions, and their
answers appear throughout analysis clearly marked as custom.

- New "Custom Fields" page off the nav drawer (leads only): list,
  create/edit with an options editor, reorder, archive/unarchive; type
  is immutable after creation
- LovatAPI custom_fields extension + in-memory definitions cache
- Raw report gains an "Asked by your team" section
- Team Lookup: number answers as an "Asked by your team" category with
  per-match detail graphs; select answers as breakdown distributions
- Picklists: number fields become weights, resilient to archived/unknown
  fields; custom weights sent via a single customWeights param and stored
  on shared picklists
- Shared "Custom" chip on picklist/breakdown surfaces; grouping headers
  mark the rest

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MangoSwirl and others added 4 commits August 2, 2026 15:07
26.0.4 is a closed App Store train, so ship the custom fields build under
26.0.6 (aligned with Lovat Collection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Tap a field row to edit it, and move Archive/Unarchive into the field's
  edit page instead of a per-row overflow menu (removed the menus on both
  the active and archived lists).
- Gray out the InsetPicker when it's disabled, so the immutable type
  selector reads as locked when editing.
- Lock existing select options when editing: their text fields and remove
  buttons are disabled (options are append-only), while new options stay
  editable.
- Surface reorder failures — revert the list and show the server's error
  message (with a mounted guard) so a rejected reorder can't look like a
  success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Link the empty state to the learn.lovat.app custom fields guide.

Co-Authored-By: Claude <noreply@anthropic.com>
Each report's note card now shows that report's Text custom answers below
a divider, labeled with the question plus the Custom marker. A card also
appears for reports that have only a text answer.

Co-Authored-By: Claude <noreply@anthropic.com>
Notes tab: merge each report's text custom answers into its note card,
redesign the cards (concise match name, neutral 'plain' surface, clear
hierarchy, subtle Custom marker), anonymize other teams' attribution as
'Scouter from <team>', and make each card open the report's raw data page
with a chevron affordance.

Raw report: show custom answers to any viewer (not just the source team),
group them under a team-aware heading, and let scouting leads of the
report's team edit text answers inline.
AllianceAnalysis.fromJson hard-cast teams, the L1/L2/L3 start-time lists, and
the fuel/ball totals, but the server returns null for these when an alliance
has too little data (production guarded every one of them at the use site).
Parse defensively (empty lists, nullable totals) and tolerate a String team
number, so one weak alliance no longer throws and blanks the whole page.
…urnament

Both queries resolve the current tournament inside queryFn but omitted it from
their StaleRefreshBuilder queryKey, so the in-memory QueryCache (keyed only on
queryKey) served the previous tournament's data after switching tournaments
(and kept it if the background refetch failed). Add the tournament key to both
keys, matching scouterScheduleQuery which already does this.
_keyEquals used element-wise != which compares nested List/Map key elements by
identity. Keys with nested collections (e.g. picklistAnalysis' flags/weights,
rebuilt fresh each build) therefore always registered as changed, causing
refetch churn on every parent rebuild and — via the _activeKey guard in the
success path — occasionally discarding completed results (stuck on skeleton).
Compare using QueryCache's own jsonEncode serialization so equality matches the
actual cache lookup.
CustomTextAnswer.fromJson hard-cast name/value as String, so a single answer
with a null or blank value (e.g. a text field answered then cleared) threw and
propagated out of the notes query, replacing the entire Notes tab with the
error view. Parse defensively (skip null/non-string/whitespace-only), matching
the raw-report surface which already hides blank text answers.
load() reset loaded/loadingTeam but not data, so on a team switch the previous
team's value persisted. If the new team's flag fetch then failed with no cache,
the catch's `data == null` guard was false — suppressing the error snackbar
(a regression from production) and leaving the flag stuck on the skeleton with
no feedback. Clear data at the start of each load.
…hint

Opened from the Notes tab, the page received canModify: note.author != null,
a heuristic that's true for any own-team member. Delete and Edit-notes were
gated on that hint while the custom-answer editor already used the authoritative
reportAnalysis.canModify, so a non-lead own-team member saw Delete/Edit buttons
the server then rejected, and the three affordances could disagree. Gate all of
them on reportAnalysis.canModify (verified identical rule to the list endpoint:
SCOUTING_LEAD and viewer team == report source team).
When customFieldsAreOwnTeam is false but the server omits customFieldsSourceTeam
(parsed to null), the raw report's custom-fields section rendered the literal
"Asked by null". Fall back to "Asked by another team" in that case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant