From bb86264e043c0bb0c035fdf80d89e52c426ef535 Mon Sep 17 00:00:00 2001 From: Daniel David <114867882+pigrammer3@users.noreply.github.com> Date: Wed, 6 Aug 2025 13:00:46 -0400 Subject: [PATCH 1/8] make all checks for schema flow through one function using event key --- primary/src/routes/dashboard.ts | 2 +- .../src/routes/manage/allianceselection.ts | 4 +-- primary/src/routes/reports.ts | 10 ++++---- scoutradioz-helpers/src/matchdatahelper.ts | 25 ++++++------------- .../types/matchdatahelper.d.ts | 4 +-- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/primary/src/routes/dashboard.ts b/primary/src/routes/dashboard.ts index 3ee6ad21..18114053 100644 --- a/primary/src/routes/dashboard.ts +++ b/primary/src/routes/dashboard.ts @@ -530,7 +530,7 @@ router.get('/allianceselection', wrap(async (req, res) => { // 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting" let cookie_key = org_key + '_' + event_year + '_cols'; let colCookie = req.cookies[cookie_key]; - let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_year, colCookie); + let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_key, colCookie); if(!scoreLayout[0]) throw 'Couldn\'t find scoringlayout in allianceselection'; diff --git a/primary/src/routes/manage/allianceselection.ts b/primary/src/routes/manage/allianceselection.ts index 69309cd4..85223cd5 100644 --- a/primary/src/routes/manage/allianceselection.ts +++ b/primary/src/routes/manage/allianceselection.ts @@ -4,7 +4,7 @@ import wrap from '../../helpers/express-async-handler'; import utilities from 'scoutradioz-utilities'; import Permissions from '../../helpers/permissions'; import { matchData as matchDataHelper } from 'scoutradioz-helpers'; -import e, { assert } from 'scoutradioz-http-errors'; +import e from 'scoutradioz-http-errors'; import type { MongoDocument } from 'scoutradioz-utilities'; import type { Ranking, AggRange, OrgTeamValue } from 'scoutradioz-types'; @@ -44,7 +44,7 @@ router.get('/', wrap(async (req, res) => { // 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting" let cookie_key = org_key + '_' + event_year + '_cols'; let colCookie = req.cookies[cookie_key]; - let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_year, colCookie); + let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(org_key, event_key, colCookie); let aggQuery = []; aggQuery.push({ $match : { org_key, event_key } }); diff --git a/primary/src/routes/reports.ts b/primary/src/routes/reports.ts index 129330ea..3c03b2a9 100644 --- a/primary/src/routes/reports.ts +++ b/primary/src/routes/reports.ts @@ -485,7 +485,7 @@ router.get('/teamintelhistory', wrap(async (req, res) => { // 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting" let cookie_key = orgKey + '_' + eventYear + '_cols'; let colCookie = req.cookies[cookie_key]; - let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventYear, colCookie); + let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventKeysArray[0], colCookie); let aggQuery = []; aggQuery.push({ $match : { 'data':{$exists:true}, 'org_key': orgKey, 'team_key': teamKey, 'year': eventYear } }); @@ -741,7 +741,7 @@ router.get('/teamdata', wrap(async (req, res) => { // 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting" let cookie_key = orgKey + '_' + eventYear + '_cols'; let colCookie = req.cookies[cookie_key]; - let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventYear, colCookie); + let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventKey, colCookie); logger.trace(`scoreLayout: ${JSON.stringify(scoreLayout)}`); @@ -788,7 +788,7 @@ router.get('/matchdata', wrap(async (req, res) => { // 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting" let cookie_key = orgKey + '_' + eventYear + '_cols'; let colCookie = req.cookies[cookie_key]; - let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventYear, colCookie, true); + let scoreLayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventKey, colCookie, true); logger.trace(`scoreLayout: ${JSON.stringify(scoreLayout)}`); @@ -838,7 +838,7 @@ router.get('/matchmetrics', wrap(async (req, res) => { // 2020-02-11, M.O'C: Combined "scoringlayout" into "layout" with an org_key & the type "matchscouting" let cookie_key = orgKey + '_' + eventYear + '_cols'; let colCookie = req.cookies[cookie_key]; - let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventYear, colCookie, true); + let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventKey, colCookie, true); let aggQuery = []; let redAllianceArray = match.alliances.red.team_keys; @@ -1313,7 +1313,7 @@ router.get('/allteammetrics', wrap(async (req, res) => { // 2020-02-15, M.O'C: Leverage column selection cookies - pull in the cookies let cookie_key = orgKey + '_' + eventYear + '_cols'; let colCookie = req.cookies[cookie_key]; - let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventYear, colCookie); + let scorelayout = await matchDataHelper.getModifiedMatchScoutingLayout(orgKey, eventKey, colCookie); // Build the aggregation data let aggQuery = []; diff --git a/scoutradioz-helpers/src/matchdatahelper.ts b/scoutradioz-helpers/src/matchdatahelper.ts index 5aee03ca..1e2dc842 100644 --- a/scoutradioz-helpers/src/matchdatahelper.ts +++ b/scoutradioz-helpers/src/matchdatahelper.ts @@ -2,7 +2,7 @@ 'use strict'; import log4js from '@log4js-node/log4js-api'; import type { Utilities, MongoDocument } from 'scoutradioz-utilities'; -import type { Match, Team, Ranking, TeamKey, AggRange, MatchFormData, PitScouting, formDataOutput, DerivedOperation, MultiplyOperation, SumOperation, SubtractOperation, DivideOperation, MultiselectOperation, ConditionOperation, CompareOperation, LogOperation, MinMaxOperation, AbsoluteValueOperation, DerivedLayout, DerivedLayoutLegacy, OrgKey, EventKey, Schema, SchemaItem, CheckBoxItem, CounterItem, DerivedItem, DerivedItemLegacy, SliderItem, HeaderItem, SubheaderItem, ImageItem, SpacerItem } from 'scoutradioz-types'; +import type { Match, Team, Ranking, TeamKey, AggRange, MatchFormData, PitScouting, formDataOutput, DerivedOperation, MultiplyOperation, SumOperation, SubtractOperation, DivideOperation, MultiselectOperation, ConditionOperation, CompareOperation, LogOperation, MinMaxOperation, AbsoluteValueOperation, OrgKey, EventKey, Schema, SchemaItem, CheckBoxItem, CounterItem, DerivedItem, DerivedItemLegacy, SliderItem, HeaderItem, SubheaderItem, ImageItem, SpacerItem } from 'scoutradioz-types'; import assert from 'assert'; import { DerivedCalculator, convertValuesDict } from './derivedhelper.js'; import ztable from 'ztable'; @@ -489,13 +489,13 @@ export class MatchDataHelper { /** * @param {string} org_key Org key - * @param {number} event_year Year of event + * @param {number} event_key Event key * @param {string} colCookie Comma-separated list of metric IDs * @return {array} Modified (reduce) match scouting layout, from the list in colCookie */ - static async getModifiedMatchScoutingLayout(org_key: string, event_year: number, colCookie: string, showAllColumns: boolean = false) { + static async getModifiedMatchScoutingLayout(org_key: string, event_key: string, colCookie: string, showAllColumns: boolean = false) { logger.addContext('funcName', 'getModifiedMatchScoutingLayout'); - logger.info('ENTER org_key=' + org_key + ',event_year=' + event_year + ',colCookie=' + colCookie); + logger.info('ENTER org_key=' + org_key + ',event_key=' + event_key + ',colCookie=' + colCookie); if (!utilities) { throw new Error('Utilities has not been configured!'); @@ -511,18 +511,7 @@ export class MatchDataHelper { // { sort: { 'order': 1 } }, // { allowCache: true } // ); - const orgschema = await utilities.findOne('orgschemas', - { org_key, year: event_year, form_type: 'matchscouting' }, - {}, - { allowCache: true, maxCacheAge: 180 } - ); - assert(orgschema); - const schema = await utilities.findOne('schemas', - { _id: orgschema.schema_id, }, - {}, - { allowCache: true, maxCacheAge: 180 } - ); - assert(schema); + const schema = await this.getSchemaForOrgAndEvent(org_key, event_key, 'matchscouting'); const scorelayoutDB = schema.layout.filter(item => MatchDataHelper.isMetric(item)); //const scorelayoutDB = schema.layout.filter(item => item.id); logger.trace(`scoreLayoutDB=${JSON.stringify(scorelayoutDB)}`); @@ -550,7 +539,7 @@ export class MatchDataHelper { } logger.trace('theseColDefaults=' + JSON.stringify(theseColDefaults)); - let defaultSet = theseColDefaults[event_year]; + let defaultSet = theseColDefaults[parseInt(event_key.slice(0, 4))]; logger.trace('defaultSet=' + defaultSet); if (defaultSet) { @@ -1029,7 +1018,7 @@ export class MatchDataHelper { let cookie_key = org_key + '_' + event_year + '_cols'; let colCookie = cookies[cookie_key]; // 2025-03-07, M.O'C: Add ability to show all columns regardless of column selections - let scorelayout = await this.getModifiedMatchScoutingLayout(org_key, event_year, colCookie, showAllColumns); + let scorelayout = await this.getModifiedMatchScoutingLayout(org_key, event_key, colCookie, showAllColumns); let aggQuery: MongoDocument[] = []; aggQuery.push({ $match: { 'team_key': { $in: teamList }, 'org_key': org_key, 'event_key': event_key } }); diff --git a/scoutradioz-helpers/types/matchdatahelper.d.ts b/scoutradioz-helpers/types/matchdatahelper.d.ts index 4331bd23..4e56e048 100644 --- a/scoutradioz-helpers/types/matchdatahelper.d.ts +++ b/scoutradioz-helpers/types/matchdatahelper.d.ts @@ -62,11 +62,11 @@ export declare class MatchDataHelper { }>; /** * @param {string} org_key Org key - * @param {number} event_year Year of event + * @param {number} event_key Event key * @param {string} colCookie Comma-separated list of metric IDs * @return {array} Modified (reduce) match scouting layout, from the list in colCookie */ - static getModifiedMatchScoutingLayout(org_key: string, event_year: number, colCookie: string, showAllColumns?: boolean): Promise; + static getModifiedMatchScoutingLayout(org_key: string, event_key: string, colCookie: string, showAllColumns?: boolean): Promise; /** * Recalculates aggregated data ranges for org & event and stores in DB * @param {string} org_key Org key From 546b7ce0a1bd7881a63db6123e8a17fe0a123fad Mon Sep 17 00:00:00 2001 From: Daniel David <114867882+pigrammer3@users.noreply.github.com> Date: Wed, 6 Aug 2025 14:59:53 -0400 Subject: [PATCH 2/8] add migration for orgschemas to use event_key instead of year --- .../scripts/dbUpdate20250806_schema_event.ts | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 primary/scripts/dbUpdate20250806_schema_event.ts diff --git a/primary/scripts/dbUpdate20250806_schema_event.ts b/primary/scripts/dbUpdate20250806_schema_event.ts new file mode 100644 index 00000000..e74d268b --- /dev/null +++ b/primary/scripts/dbUpdate20250806_schema_event.ts @@ -0,0 +1,177 @@ +import type { Utilities } from 'scoutradioz-utilities'; +import type { Event, Org } from 'scoutradioz-types'; +import type { ObjectId } from 'mongodb'; + +process.env.TIER = 'dev'; + +const utilities: Utilities = require('scoutradioz-utilities'); + +utilities.config(require('../databases.json'), { + cache: { + enable: true, + maxAge: 30 + }, + debug: true, +}); + +utilities.refreshTier(); + +// Local types for orgschemas only to avoid dependency on changing external schema +interface OldOrgSchema { + _id?: ObjectId; + org_key: string; + year: number; + form_type: 'matchscouting' | 'pitscouting'; + schema_id: ObjectId; +} + +interface NewOrgSchema { + org_key: string; + event_key: string; + form_type: 'matchscouting' | 'pitscouting'; + schema_id: ObjectId; +} + +(async () => { + console.log('Starting migration to convert orgschemas from year-based to event-based...'); + + // Get all current orgschemas + const currentOrgSchemas: OldOrgSchema[] = await utilities.find('orgschemas', {}); + console.log(`Found ${currentOrgSchemas.length} existing orgschemas`); + + if (currentOrgSchemas.length === 0) { + console.log('No orgschemas found to migrate'); + process.exit(0); + } + + // Get all events to create a year -> events mapping + const allEvents: Event[] = await utilities.find('events', {}); + const eventsByYear: { [year: number]: Event[] } = {}; + for (const event of allEvents) { + if (!eventsByYear[event.year]) { + eventsByYear[event.year] = []; + } + eventsByYear[event.year].push(event); + } + console.log(`Found events for years: ${Object.keys(eventsByYear).join(', ')}`); + + // For each orgschema, find all the events where it's being used + const newOrgSchemas: NewOrgSchema[] = []; + const schemasToRemove: OldOrgSchema[] = []; + + for (const orgSchema of currentOrgSchemas) { + + // Find events where this org has scouting data for this year + let eventsWithData: string[] = []; + + if (orgSchema.form_type === 'matchscouting') { + // Look for match scouting data + const matchEvents = await utilities.distinct('matchscouting', 'event_key', { + org_key: orgSchema.org_key, + year: orgSchema.year + }); + eventsWithData = eventsWithData.concat(matchEvents); + } + else if (orgSchema.form_type === 'pitscouting') { + // Look for pit scouting data + const pitEvents = await utilities.distinct('pitscouting', 'event_key', { + org_key: orgSchema.org_key, + year: orgSchema.year + }); + eventsWithData = eventsWithData.concat(pitEvents); + } + + // Remove duplicates + eventsWithData = [...new Set(eventsWithData)]; + + if (eventsWithData.length === 0) { + // No data found - let's check if the org has an event_key set for this year + const yearEvents = eventsByYear[orgSchema.year] || []; + + // Get the org's current event_key + const org: Org | null = await utilities.findOne('orgs', { org_key: orgSchema.org_key }); + const orgCurrentEventKey = org?.event_key; + + if (orgCurrentEventKey) { + // Check if the org's current event is in the correct year + const orgCurrentEvent = yearEvents.find(event => event.key === orgCurrentEventKey); + + if (orgCurrentEvent) { + // Create orgschema for the org's current event + const newOrgSchema: NewOrgSchema = { + org_key: orgSchema.org_key, + event_key: orgCurrentEventKey, + form_type: orgSchema.form_type, + schema_id: orgSchema.schema_id + }; + newOrgSchemas.push(newOrgSchema); + } + else { + // Fallback: pick the first event in the year + if (yearEvents.length > 0) { + const fallbackEvent = yearEvents[0]; + const newOrgSchema: NewOrgSchema = { + org_key: orgSchema.org_key, + event_key: fallbackEvent.key, + form_type: orgSchema.form_type, + schema_id: orgSchema.schema_id + }; + newOrgSchemas.push(newOrgSchema); + } + } + } + else if (yearEvents.length > 0) { + // Org has no current event_key set, fallback to first event of the year + const fallbackEvent = yearEvents[0]; + const newOrgSchema: NewOrgSchema = { + org_key: orgSchema.org_key, + event_key: fallbackEvent.key, + form_type: orgSchema.form_type, + schema_id: orgSchema.schema_id + }; + newOrgSchemas.push(newOrgSchema); + } + else { + console.log(` No events found for year ${orgSchema.year}, skipping orgschema for ${orgSchema.org_key}`); + } + } + else { + // Create new orgschemas for each event where data was found + for (const event_key of eventsWithData) { + const newOrgSchema: NewOrgSchema = { + org_key: orgSchema.org_key, + event_key: event_key, + form_type: orgSchema.form_type, + schema_id: orgSchema.schema_id + }; + newOrgSchemas.push(newOrgSchema); + } + } + + // Mark the old orgschema for removal + schemasToRemove.push(orgSchema); + } + + console.log('\nMigration plan:'); + console.log(` - Remove ${schemasToRemove.length} old year-based orgschemas`); + console.log(` - Create ${newOrgSchemas.length} new event-based orgschemas`); + + // Perform the migration + console.log('\nExecuting migration...'); + + // Remove all old orgschemas + const removeResult = await utilities.remove('orgschemas', {}); + console.log(`Removed ${removeResult.deletedCount} old orgschemas`); + + // Insert new event-based orgschemas + if (newOrgSchemas.length > 0) { + const insertResult = await utilities.insert('orgschemas', newOrgSchemas as any); + console.log(`Inserted ${(insertResult?.insertedCount || newOrgSchemas.length)} new orgschemas`); + } + + // Verification: Check that we can still find orgschemas + const finalSchemas = await utilities.find('orgschemas', {}); + console.log(`Migration complete! Final orgschema count: ${finalSchemas.length}`); + + process.exit(0); +})(); From 604534d608e10c7a5dd989e268d3e55ab8fa7686 Mon Sep 17 00:00:00 2001 From: Daniel David <114867882+pigrammer3@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:00:17 -0400 Subject: [PATCH 3/8] update types to use event_key instead of year in orgschema --- scoutradioz-types/types.d.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scoutradioz-types/types.d.ts b/scoutradioz-types/types.d.ts index 6b5dd216..a4c8f408 100644 --- a/scoutradioz-types/types.d.ts +++ b/scoutradioz-types/types.d.ts @@ -224,12 +224,11 @@ export declare interface Schema extends DbDocument { } /** - * Ties an org to a layout key - * TODO: link org+event instead of just org+year + * Ties an org to a layout key for a specific event */ export declare interface OrgSchema extends DbDocument { org_key: OrgKey; - year: number; + event_key: EventKey; form_type: 'matchscouting'|'pitscouting'; /** ID of the item in the Schemas database */ schema_id: ObjectId; From b70fa3bbfc51cc6a178b832f458ec52037342719 Mon Sep 17 00:00:00 2001 From: Daniel David <114867882+pigrammer3@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:00:38 -0400 Subject: [PATCH 4/8] update matchdatahelper to use event_key instead of year, had to change some func sigs too --- scoutradioz-helpers/src/matchdatahelper.ts | 21 ++++++------------- .../types/matchdatahelper.d.ts | 4 ++-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/scoutradioz-helpers/src/matchdatahelper.ts b/scoutradioz-helpers/src/matchdatahelper.ts index 1e2dc842..19420137 100644 --- a/scoutradioz-helpers/src/matchdatahelper.ts +++ b/scoutradioz-helpers/src/matchdatahelper.ts @@ -412,11 +412,11 @@ export class MatchDataHelper { /** * Calculate derived metrics for a provided array of match data items. * @param {string} org_key Org key - * @param {number} event_year Year of event + * @param {string} event_key Event key * @param {Object} matchData Scouting data ("data" field in the db) * @returns {Object} matchData - Same object, not cloned, with the derived metrics added */ - static async calculateDerivedMetrics(org_key: string, event_year: number, matchData: MatchFormData) { + static async calculateDerivedMetrics(org_key: string, event_key: string, matchData: MatchFormData) { // let st = performance.now(); // Just derived fields from the org's match scouting layout for this year // let derivedLayout = await utilities.find('layout', @@ -427,7 +427,7 @@ export class MatchDataHelper { let t_dbStart = performance.now(); const orgschema = await utilities.findOne('orgschemas', - { org_key, year: event_year, form_type: 'matchscouting' }, + { org_key, event_key, form_type: 'matchscouting' }, {}, { allowCache: true, maxCacheAge: 180 } ); @@ -603,7 +603,7 @@ export class MatchDataHelper { // { allowCache: true } // ); const orgschema = await utilities.findOne('orgschemas', - { org_key, year: event_year, form_type: 'matchscouting' }, + { org_key, event_key, form_type: 'matchscouting' }, {}, { allowCache: true, maxCacheAge: 180 } ); @@ -1226,17 +1226,10 @@ export class MatchDataHelper { * @param event_key */ static async getSchemaForOrgAndEvent(org_key: OrgKey, event_key: EventKey, form_type: Schema['form_type']): Promise { - // later, this lookup won't be needed because orgschemas will be org+event rather than org+year - const { year } = await utilities.findOne('events', - { key: event_key }, - {}, - { allowCache: true, maxCacheAge: 180 } - ); - const orgschema = await utilities.findOne('orgschemas', - { org_key, year, form_type }, + { org_key, event_key, form_type }, ); - assert(orgschema, `${form_type} schema not found for ${org_key} and ${year}!`); + assert(orgschema, `${form_type} schema not found for ${org_key} and ${event_key}!`); const schema = await utilities.findOne('schemas', { _id: orgschema.schema_id, }, @@ -1249,8 +1242,6 @@ export class MatchDataHelper { // make sure org has permission to use this schema (either an owner of unpublished schema, OR it is published) assert(schema.owners.includes(org_key) || schema.published, `Org ${org_key} does not have permission to use un-published schema "${schema.name}"!`); - // todo: maybe we don't need this match? maybe we can remove the "year" field from Schema? - assert(schema.year === year, `Schema year ${schema.year} and event year ${year} do not match!`); return schema; } diff --git a/scoutradioz-helpers/types/matchdatahelper.d.ts b/scoutradioz-helpers/types/matchdatahelper.d.ts index 4e56e048..c9ec99f9 100644 --- a/scoutradioz-helpers/types/matchdatahelper.d.ts +++ b/scoutradioz-helpers/types/matchdatahelper.d.ts @@ -47,11 +47,11 @@ export declare class MatchDataHelper { /** * Calculate derived metrics for a provided array of match data items. * @param {string} org_key Org key - * @param {number} event_year Year of event + * @param {string} event_key Event key * @param {Object} matchData Scouting data ("data" field in the db) * @returns {Object} matchData - Same object, not cloned, with the derived metrics added */ - static calculateDerivedMetrics(org_key: string, event_year: number, matchData: MatchFormData): Promise<{ + static calculateDerivedMetrics(org_key: string, event_key: string, matchData: MatchFormData): Promise<{ matchData: MatchFormData; db: number; constructor: number; From 8d580b3ceb506bf418e1b98eb3a1c44b99cd8c59 Mon Sep 17 00:00:00 2001 From: Daniel David <114867882+pigrammer3@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:01:06 -0400 Subject: [PATCH 5/8] update voyager & primary to use new orgschema format --- primary/src/routes/admin/sync.ts | 2 +- primary/src/routes/manage/orgconfig.ts | 71 ++++++++++++++----- primary/src/routes/scouting.ts | 2 +- voyager/src/lib/localDB.ts | 2 +- .../submit/match/+server.ts | 2 +- .../layout/[type=form_type]/+server.ts | 8 ++- 6 files changed, 64 insertions(+), 23 deletions(-) diff --git a/primary/src/routes/admin/sync.ts b/primary/src/routes/admin/sync.ts index 82931b99..ce9f391d 100644 --- a/primary/src/routes/admin/sync.ts +++ b/primary/src/routes/admin/sync.ts @@ -330,7 +330,7 @@ router.get('/recalcderived', wrap(async (req, res) => { ttokenize, tparse, tresolve, - } = await matchDataHelper.calculateDerivedMetrics(org_key, event_year, thisScored.data); + } = await matchDataHelper.calculateDerivedMetrics(org_key, event_key, thisScored.data); times.db += db; times.constructor += constructor; diff --git a/primary/src/routes/manage/orgconfig.ts b/primary/src/routes/manage/orgconfig.ts index 94d979a3..e38effe0 100644 --- a/primary/src/routes/manage/orgconfig.ts +++ b/primary/src/routes/manage/orgconfig.ts @@ -3,7 +3,7 @@ import express from 'express'; import { getLogger } from 'log4js'; import e, { HttpError, assert } from 'scoutradioz-http-errors'; import { upload as uploadHelper } from 'scoutradioz-helpers'; -import type { Layout, MatchFormData, MatchScouting, OrgSchema, SchemaItem, Schema, SprCalculation, Upload } from 'scoutradioz-types'; +import type { Layout, MatchFormData, MatchScouting, OrgSchema, Schema, SprCalculation, Upload } from 'scoutradioz-types'; import type { MongoDocument } from 'scoutradioz-utilities'; import utilities from 'scoutradioz-utilities'; import wrap from '../../helpers/express-async-handler'; @@ -170,14 +170,9 @@ router.get('/editform', wrap(async (req, res) => { let org_key = req._user.org_key; - let year = parseInt(String(req.query.year)) || req.event.year; - if (!year || isNaN(year)) throw new e.UserError('Either "year" or "key" must be set.'); - - if (year === -1) { - let currentYear = new Date().getFullYear(); - logger.debug(`Year is -1, aka, event not set. Setting year to current year: ${currentYear}`); - year = currentYear; - } + // Use the current event for form editing + let event_key = req.event.key; + if (!event_key) throw new e.UserError('No current event set.'); // load form definition data from the database let schema: Schema | undefined, @@ -199,25 +194,62 @@ router.get('/editform', wrap(async (req, res) => { }`; const orgschema = await utilities.findOne('orgschemas', - { org_key, year, form_type }, + { org_key, event_key, form_type }, ); if (orgschema) { schema = await utilities.findOne('schemas', { _id: orgschema.schema_id, owners: org_key }, ); - assert(schema, `For ${org_key} and ${year}, orgschema existed in the database but pointed to nonexistent schema!`); + assert(schema, `For ${org_key} and ${event_key}, orgschema existed in the database but pointed to nonexistent schema!`); // Create string representation of layout layout = JSON.stringify(schema.layout).replace(/`/g, '\\`'); // 2025-02-01, M.O'C: Only do if SPR calculation exists if (schema.spr_calculation) sprLayout = JSON.stringify(schema.spr_calculation).replace(/`/g, '\\`'); else - logger.info(`For ${org_key} and ${year}, orgschema existed in the database but had no SPR calculation - using default`); + logger.info(`For ${org_key} and ${event_key}, orgschema existed in the database but had no SPR calculation - using default`); + } + else { + // No schema for current event - check if org has schemas from other events in the same year + logger.info(`No orgschema found for ${org_key} at ${event_key}, checking for schemas from other events in year ${req.event.year}`); + + // Find all events in the same year + const eventsInYear = await utilities.find('events', { year: req.event.year }); + const eventKeysInYear = eventsInYear.map(event => event.key); + + // Find orgschemas for this org and form_type from any event in the same year + const orgSchemasInYear = await utilities.find('orgschemas', { + org_key, + form_type, + event_key: { $in: eventKeysInYear } + }); + + if (orgSchemasInYear.length > 0) { + // Get the schemas and find the most recently updated one + const schemaIds = orgSchemasInYear.map(os => os.schema_id); + const schemasInYear = await utilities.find('schemas', { + _id: { $in: schemaIds }, + owners: org_key + }, { sort: { last_modified: -1 } }); // Sort by most recent first + + if (schemasInYear.length > 0) { + const mostRecentSchema = schemasInYear[0]; + logger.info(`Found ${schemasInYear.length} existing schemas for ${org_key} in year ${req.event.year}, using most recent from ${mostRecentSchema.last_modified}`); + + // Use the most recent schema as a template + layout = JSON.stringify(mostRecentSchema.layout).replace(/`/g, '\\`'); + if (mostRecentSchema.spr_calculation) + sprLayout = JSON.stringify(mostRecentSchema.spr_calculation).replace(/`/g, '\\`'); + + // Don't set the schema variable since we want to create a new one, just use the layout + logger.info(`Using existing schema layout as template for new event ${event_key}`); + } + } } // Get name, description, and whether it's published from the schema (or assign defaults) let { name, description, published } = schema || { - name: `${org_key}'s ${year} ${form_type} Form`, + name: `${org_key}'s ${event_key} ${form_type} Form`, description: '', published: false }; @@ -226,7 +258,7 @@ router.get('/editform', wrap(async (req, res) => { let existingFormData = new Map(); let previousDataExists = false; // get existing data schema (if any) - let matchDataFind: MatchScouting[] = await utilities.find('matchscouting', { org_key, year, 'data': { $exists: true } }, {}); + let matchDataFind: MatchScouting[] = await utilities.find('matchscouting', { org_key, event_key, 'data': { $exists: true } }, {}); matchDataFind.forEach((element) => { let thisMatch: MatchScouting = element; if (thisMatch['data']) { @@ -267,7 +299,8 @@ router.get('/editform', wrap(async (req, res) => { published, form_type, org_key, - year, + event_key, + year: req.event.year, previousDataExists, previousKeys }); @@ -297,6 +330,10 @@ router.post('/submitform', wrap(async (req, res) => { assert(!isNaN(year), 'invalid year!'); assert(['matchscouting', 'pitscouting'].includes(form_type), 'invalid form_type!'); + // Convert year to event_key using current event + const event_key = req.event.key; + if (!event_key) throw new e.UserError('No current event set.'); + // Get the list of org images (for checking image IDs in form) const orgImages = await uploadHelper.findOrgImages(org_key, year); const orgImageKeys = Object.keys(orgImages); @@ -337,7 +374,7 @@ router.post('/submitform', wrap(async (req, res) => { // Get existing schema metadata from db const orgschema = await utilities.findOne('orgschemas', - { org_key, year, form_type }, + { org_key, event_key, form_type }, ); // schema did exist in db; update it now if (orgschema) { @@ -382,7 +419,7 @@ router.post('/submitform', wrap(async (req, res) => { let newOrgSchema: OrgSchema = { org_key, - year, + event_key, form_type, schema_id: insertResult.insertedId, }; diff --git a/primary/src/routes/scouting.ts b/primary/src/routes/scouting.ts index 98fdb355..ed2e025f 100644 --- a/primary/src/routes/scouting.ts +++ b/primary/src/routes/scouting.ts @@ -248,7 +248,7 @@ router.post('/match/submit', wrap(async (req, res) => { logger.debug('matchData(UPDATED:1)=' + JSON.stringify(correctedData)); // 2022-02-22, JL: Moved dervied metric calculations into matchDataHelper - let { matchData: matchDataNew } = await matchDataHelper.calculateDerivedMetrics(org_key, event_year, correctedData); + let { matchData: matchDataNew } = await matchDataHelper.calculateDerivedMetrics(org_key, event_key, correctedData); correctedData = matchDataNew; // JL: this is temporary while we have debugging info logger.debug('matchData(UPDATED:2)=' + JSON.stringify(correctedData)); diff --git a/voyager/src/lib/localDB.ts b/voyager/src/lib/localDB.ts index 8656c99b..072e4f4e 100644 --- a/voyager/src/lib/localDB.ts +++ b/voyager/src/lib/localDB.ts @@ -234,7 +234,7 @@ export class LocalDB extends Dexie { schemas: '&_id', teams: '&key, team_number', orgs: '&org_key', - orgschemas: '&[org_key+year+form_type]', + orgschemas: '&[org_key+event_key+form_type]', uploads: '&_id, [org_key+year]', images: '&s3_key', diff --git a/voyager/src/routes/api/orgs/[org_key]/[event_key=event_key]/submit/match/+server.ts b/voyager/src/routes/api/orgs/[org_key]/[event_key=event_key]/submit/match/+server.ts index 53c81d2e..f994e7d2 100644 --- a/voyager/src/routes/api/orgs/[org_key]/[event_key=event_key]/submit/match/+server.ts +++ b/voyager/src/routes/api/orgs/[org_key]/[event_key=event_key]/submit/match/+server.ts @@ -114,7 +114,7 @@ export const POST: RequestHandler = async ({ } console.debug('data(UPDATED:1)=', JSON.stringify(data)); console.log(matchDataHelper.fixDatumType, matchDataHelper.calculateDerivedMetrics); - let { matchData } = await matchDataHelper.calculateDerivedMetrics(org_key, event.year, data); + let { matchData } = await matchDataHelper.calculateDerivedMetrics(org_key, event_key, data); data = matchData; console.debug('data(UPDATED:2)=', JSON.stringify(data)); diff --git a/voyager/src/routes/api/orgs/[org_key]/[event_year=integer]/layout/[type=form_type]/+server.ts b/voyager/src/routes/api/orgs/[org_key]/[event_year=integer]/layout/[type=form_type]/+server.ts index 2292d757..5efe7bd3 100644 --- a/voyager/src/routes/api/orgs/[org_key]/[event_year=integer]/layout/[type=form_type]/+server.ts +++ b/voyager/src/routes/api/orgs/[org_key]/[event_year=integer]/layout/[type=form_type]/+server.ts @@ -9,12 +9,16 @@ export const GET: RequestHandler = async ({ params, locals }) => { validateUserOrg(locals, params.org_key); const form_type = (params.type + 'scouting') as 'pitscouting'|'matchscouting'; + // Get the current event for this org to find the event_key + const org = await utilities.findOne('orgs', { org_key: params.org_key }); + httpAssert(org?.event_key, 404, `No current event set for org ${params.org_key}`); + const orgschema = await utilities.findOne('orgschemas', - {org_key: params.org_key, form_type, year: parseInt(params.event_year)}, + {org_key: params.org_key, form_type, event_key: org.event_key}, {}, { allowCache: true }, ); - httpAssert(orgschema, 500, `Org schema not found, ${params.org_key}, ${params.event_year}`); + httpAssert(orgschema, 500, `Org schema not found, ${params.org_key}, ${org.event_key}`); const schema = await utilities.findOne('schemas', {_id: orgschema.schema_id}, {}, From 1eb16a01a27f6503ada8b4972d213ff429f964f9 Mon Sep 17 00:00:00 2001 From: Daniel David <114867882+pigrammer3@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:06:31 -0400 Subject: [PATCH 6/8] add number input to voyager --- scoutradioz-types/types.d.ts | 4 ++++ voyager/src/lib/form/NumberInput.svelte | 22 ++++++++++++++++++++++ voyager/src/lib/form/ScoutingForm.svelte | 7 +++++++ 3 files changed, 33 insertions(+) create mode 100644 voyager/src/lib/form/NumberInput.svelte diff --git a/scoutradioz-types/types.d.ts b/scoutradioz-types/types.d.ts index a4c8f408..d8cfaf5b 100644 --- a/scoutradioz-types/types.d.ts +++ b/scoutradioz-types/types.d.ts @@ -133,6 +133,10 @@ export interface TextBlockItem extends FormInputItem { type: 'textblock'; } +export interface NumberInputItem extends FormInputItem { + type: 'number'; +} + export interface CheckBoxItem extends FormInputItem { type: 'checkbox'; } diff --git a/voyager/src/lib/form/NumberInput.svelte b/voyager/src/lib/form/NumberInput.svelte new file mode 100644 index 00000000..945e37d9 --- /dev/null +++ b/voyager/src/lib/form/NumberInput.svelte @@ -0,0 +1,22 @@ + + + + diff --git a/voyager/src/lib/form/ScoutingForm.svelte b/voyager/src/lib/form/ScoutingForm.svelte index 58622fd4..64d215d4 100644 --- a/voyager/src/lib/form/ScoutingForm.svelte +++ b/voyager/src/lib/form/ScoutingForm.svelte @@ -7,6 +7,7 @@ import Slider from './Slider.svelte'; import Textblock from './Textblock.svelte'; import Image from './Image.svelte'; + import NumberInput from './NumberInput.svelte'; interface Props { layout: SchemaItem[]; @@ -60,6 +61,12 @@ {field} {onchange} /> + {:else if field.type === 'number'} + {:else if field.type === 'image'} Date: Wed, 6 Aug 2025 17:21:52 -0400 Subject: [PATCH 7/8] add number input --- primary/views/scouting/match.pug | 2 ++ primary/views/scouting/pit.pug | 2 ++ primary/views/scouting/supermatch.pug | 2 ++ primary/views/scouting/superpit.pug | 2 ++ primary/views/scouting/templates/formNumberInput.pug | 6 ++++++ scoutradioz-helpers/src/jsonlayout.ts | 11 ++++++++++- scoutradioz-types/types.d.ts | 2 +- 7 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 primary/views/scouting/templates/formNumberInput.pug diff --git a/primary/views/scouting/match.pug b/primary/views/scouting/match.pug index 5b78888a..6c6aae42 100644 --- a/primary/views/scouting/match.pug +++ b/primary/views/scouting/match.pug @@ -59,6 +59,8 @@ block content include templates/formMultiselect when "textblock" include templates/formTextBlock + when "number" + include templates/formNumberInput when "header" +Header(element) when "subheader" diff --git a/primary/views/scouting/pit.pug b/primary/views/scouting/pit.pug index f1798e48..01412b01 100644 --- a/primary/views/scouting/pit.pug +++ b/primary/views/scouting/pit.pug @@ -93,6 +93,8 @@ block content include templates/formMultiselect when "textblock" include templates/formTextBlock + when "number" + include templates/formNumberInput when "header" +Header(element) when "subheader" diff --git a/primary/views/scouting/supermatch.pug b/primary/views/scouting/supermatch.pug index 554ea5c7..678853c6 100644 --- a/primary/views/scouting/supermatch.pug +++ b/primary/views/scouting/supermatch.pug @@ -46,6 +46,8 @@ block content include templates/formMultiselect when "textblock" include templates/formTextBlock + when "number" + include templates/formNumberInput when "header" +Header(element) when "subheader" diff --git a/primary/views/scouting/superpit.pug b/primary/views/scouting/superpit.pug index a9cbcbb4..50c58b62 100644 --- a/primary/views/scouting/superpit.pug +++ b/primary/views/scouting/superpit.pug @@ -40,6 +40,8 @@ block content include templates/formMultiselect when "textblock" include templates/formTextBlock + when "number" + include templates/formNumberInput when "header" +Header(element) when "subheader" diff --git a/primary/views/scouting/templates/formNumberInput.pug b/primary/views/scouting/templates/formNumberInput.pug new file mode 100644 index 00000000..2af84897 --- /dev/null +++ b/primary/views/scouting/templates/formNumberInput.pug @@ -0,0 +1,6 @@ +h5=element.label +- if ( !answer || isNaN(answer) ) + - answer = 0; +input(class="theme-input theme-inline-padding w3-no-border w3-half" style="float: none" type="number" name=element.id)=answer +br +br \ No newline at end of file diff --git a/scoutradioz-helpers/src/jsonlayout.ts b/scoutradioz-helpers/src/jsonlayout.ts index 6bb73a32..69dfdbfa 100644 --- a/scoutradioz-helpers/src/jsonlayout.ts +++ b/scoutradioz-helpers/src/jsonlayout.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import type { SprCalculation, CheckBoxItem, CounterItem, DerivedItem, DerivedItemLegacy, HeaderItem, LayoutEdit, MatchFormData, MultiselectItem, SchemaItem, SliderItem, ImageItem, SpacerItem, StringDict, SubheaderItem, TextBlockItem } from 'scoutradioz-types'; +import type { SprCalculation, CheckBoxItem, CounterItem, DerivedItem, DerivedItemLegacy, HeaderItem, LayoutEdit, MatchFormData, MultiselectItem, SchemaItem, SliderItem, ImageItem, SpacerItem, StringDict, SubheaderItem, TextBlockItem, NumberInputItem } from 'scoutradioz-types'; import { convertValuesDict, DerivedCalculator } from './derivedhelper.js'; const validTypes = ['checkbox', 'counter', 'slider', 'multiselect', 'textblock', 'header', 'subheader', 'spacer', 'derived', 'image']; @@ -119,6 +119,10 @@ export function validateJSONLayout(layout: SchemaItem[], orgImageKeys: string[]) validateTextBlock(item); testData[item.id] = ''; break; + case 'number': + validateNumberInput(item); + testData[item.id] = 0; + break; case 'derived': validateDerived(item); break; @@ -211,6 +215,11 @@ export function validateJSONLayout(layout: SchemaItem[], orgImageKeys: string[]) checkId(item); } + function validateNumberInput(item: NumberInputItem) { + checkExpectedKeys(item, ['type', 'id', 'label'], true); + checkId(item); + } + function validateCheckbox(item: CheckBoxItem) { checkExpectedKeys(item, ['type', 'id', 'label'], true); checkId(item); diff --git a/scoutradioz-types/types.d.ts b/scoutradioz-types/types.d.ts index d8cfaf5b..9519e7a0 100644 --- a/scoutradioz-types/types.d.ts +++ b/scoutradioz-types/types.d.ts @@ -197,7 +197,7 @@ export interface DerivedItem extends SchemaItemBase { * Note: Since some types don't need an id, you may get typescript errors when trying to access the id property. * Use e.g. `if ('id' in item)` to check if the item has an id, as that "in" syntax is preferred by TS */ -export type SchemaItem = TextBlockItem | CheckBoxItem | CounterItem | SliderItem | CounterItem | MultiselectItem | HeaderItem | SubheaderItem | ImageItem | SpacerItem | DerivedItem | DerivedItemLegacy; +export type SchemaItem = NumberInputItem | TextBlockItem | CheckBoxItem | CounterItem | SliderItem | CounterItem | MultiselectItem | HeaderItem | SubheaderItem | ImageItem | SpacerItem | DerivedItem | DerivedItemLegacy; /** * Sub-object SPR calculation From 774ee230168a4254ba833f057c6e80cfa4db04c8 Mon Sep 17 00:00:00 2001 From: Daniel David <114867882+pigrammer3@users.noreply.github.com> Date: Sat, 16 Aug 2025 18:34:06 -0400 Subject: [PATCH 8/8] update all of year csv export to include every field in some form that year --- primary/src/routes/reports.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/primary/src/routes/reports.ts b/primary/src/routes/reports.ts index 3c03b2a9..d878a41c 100644 --- a/primary/src/routes/reports.ts +++ b/primary/src/routes/reports.ts @@ -960,7 +960,6 @@ router.get('/metricsranked', wrap(async (req, res) => { logger.addContext('funcName', 'metricsranked[get]'); logger.info('ENTER'); - let eventYear = req.event.year; let orgKey = req._user.org_key; let eventKey = req.event.key; @@ -1092,7 +1091,6 @@ router.get('/metrics', wrap(async (req, res) => { logger.addContext('funcName', 'metrics[get]'); logger.info('ENTER'); - let eventYear = req.event.year; const org_key = req._user.org_key; // for later querying by event_key @@ -1445,7 +1443,21 @@ router.get('/exportdata', wrap(async (req, res) => { logger.info('ENTER event_key=' + eventKey + ',org_key=' + orgKey + ',data_type=' + dataType + ',dataSpan=' + dataSpan + ',req.shortagent=' + JSON.stringify(req.shortagent)); // read in the list of form options - const { layout: exportLayout } = await matchDataHelper.getSchemaForOrgAndEvent(orgKey, eventKey, dataType); + + const exportLayout: SchemaItem[] = []; + + for (const orgschema of await utilities.find('orgschemas', {org_key: orgKey, form_type: dataType, event_key: { $regex: '^' + eventYear }})) { + const schema = await utilities.findOne('schemas', { _id: orgschema.schema_id }); + if (schema) { + schema.layout.forEach(item => { + if (matchDataHelper.isMetric(item) && !exportLayout.some( + existingItem => matchDataHelper.isMetric(existingItem) && existingItem.id === item.id + )) { + exportLayout.push(item); + } + }); + } + } // sanity check //logger.debug("layout=" + JSON.stringify(exportLayout));