Shoraky commited on
Commit
dce0b2f
·
1 Parent(s): 618f1a0

Normalize player ratings to a 100-point scale

Browse files
Files changed (1) hide show
  1. src/server.ts +33 -8
src/server.ts CHANGED
@@ -63,6 +63,11 @@ const actionReviewSchema = z.object({
63
  feedback: z.array(z.string().trim().min(1).max(2000)).max(30),
64
  }).strict();
65
 
 
 
 
 
 
66
 
67
  async function requireActiveSubscription(req: Request, res: Response) {
68
  const current = await authenticatedUser(req, res);
@@ -150,12 +155,12 @@ app.get("/api/players-overview", async (req, res, next) => {
150
  const byAction = [...new Set(playerActions.map((action) => action.type))].map((type) => {
151
  const matching = playerActions.filter((action) => action.type === type);
152
  const values = matching.map((action) => scoresByAction.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
153
- return { axis: type, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, samples: values.length };
154
  });
155
  const playerScoreSessions = playerSessions.map((session) => {
156
  const sessionActions = actionsBySession.get(session.session_id) ?? [];
157
  const values = sessionActions.map((action) => scoresByAction.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
158
- return { sessionId: session.session_id, createdAt: Date.parse(session.created_at) || 0, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, actionCount: sessionActions.length, scoredCount: values.length };
159
  });
160
  return {
161
  playerId: row.player_id,
@@ -170,7 +175,7 @@ app.get("/api/players-overview", async (req, res, next) => {
170
  scores: {
171
  playerId: row.player_id, rubricVersion: "database", totalActions: playerActions.length,
172
  scoredActions: scored.length, byAction, byQuality: [],
173
- overall: scored.length ? scored.reduce((sum, value) => sum + value, 0) / scored.length : null,
174
  sessions: playerScoreSessions,
175
  },
176
  };
@@ -215,18 +220,38 @@ app.get("/api/players-overview/:playerId", async (req, res, next) => {
215
  const byAction = [...new Set(ownActions.map((row) => row.type))].map((type) => {
216
  const matching = ownActions.filter((row) => row.type === type);
217
  const values = matching.map((row) => scoreMap.get(row.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
218
- return { axis: type, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, samples: values.length };
 
 
 
 
 
 
 
 
 
 
219
  });
220
  const player = overview?.find((row) => row.player_id === playerId.data);
221
  if (!player) return res.status(404).json({ error: "Player not found." });
222
  const sessionsForPlayer = ownSessions.map((row) => {
223
  const sessionActions = ownActions.filter((action) => action.session_id === row.session_id);
224
  const values = sessionActions.map((action) => scoreMap.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
225
- return { sessionId: row.session_id, createdAt: Date.parse(row.created_at) || 0, score: values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null, actionCount: sessionActions.length, scoredCount: values.length };
226
  });
227
  const { data: peers, error: peersError } = await supabaseAdmin.from("players").select("player_id");
228
  if (peersError) throw new Error(`Unable to load roster: ${peersError.message}`);
229
- const squadAverage = { byQuality: [], byAction };
 
 
 
 
 
 
 
 
 
 
230
  return res.json({
231
  playerId: player.player_id, name: [player.fname, player.lname].filter(Boolean).join(" "),
232
  age: player.bdate ? String(Math.max(0, new Date().getFullYear() - new Date(player.bdate).getFullYear())) : "",
@@ -234,8 +259,8 @@ app.get("/api/players-overview/:playerId", async (req, res, next) => {
234
  club: player.club ?? "", position: player.position ?? "", dominantFoot: "", notes: "", avatarPath: "",
235
  createdAt: "", updatedAt: "", hasProfile: true, sessionCount: ownSessions.length, actionCount: ownActions.length,
236
  lastSessionAt: sessionsForPlayer[0]?.createdAt ?? 0,
237
- scores: { playerId: player.player_id, rubricVersion: "database", totalActions: ownActions.length, scoredActions: ownScores.length, byAction, byQuality: [], overall: ownScores.length ? ownScores.reduce((sum, value) => sum + value, 0) / ownScores.length : null, sessions: sessionsForPlayer },
238
- squadAverage, rank: null, playerCount: peers?.length ?? 0,
239
  });
240
  } catch (error) { next(error); }
241
  });
 
63
  feedback: z.array(z.string().trim().min(1).max(2000)).max(30),
64
  }).strict();
65
 
66
+ // Stored coach ratings use a 0–10 scale. Player dashboards use a 0–100 scale.
67
+ const meanPlayerScore = (values: number[]) => values.length
68
+ ? values.reduce((sum, value) => sum + value, 0) / values.length * 10
69
+ : null;
70
+
71
 
72
  async function requireActiveSubscription(req: Request, res: Response) {
73
  const current = await authenticatedUser(req, res);
 
155
  const byAction = [...new Set(playerActions.map((action) => action.type))].map((type) => {
156
  const matching = playerActions.filter((action) => action.type === type);
157
  const values = matching.map((action) => scoresByAction.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
158
+ return { axis: type, score: meanPlayerScore(values), samples: values.length };
159
  });
160
  const playerScoreSessions = playerSessions.map((session) => {
161
  const sessionActions = actionsBySession.get(session.session_id) ?? [];
162
  const values = sessionActions.map((action) => scoresByAction.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
163
+ return { sessionId: session.session_id, createdAt: Date.parse(session.created_at) || 0, score: meanPlayerScore(values), actionCount: sessionActions.length, scoredCount: values.length };
164
  });
165
  return {
166
  playerId: row.player_id,
 
175
  scores: {
176
  playerId: row.player_id, rubricVersion: "database", totalActions: playerActions.length,
177
  scoredActions: scored.length, byAction, byQuality: [],
178
+ overall: meanPlayerScore(scored),
179
  sessions: playerScoreSessions,
180
  },
181
  };
 
220
  const byAction = [...new Set(ownActions.map((row) => row.type))].map((type) => {
221
  const matching = ownActions.filter((row) => row.type === type);
222
  const values = matching.map((row) => scoreMap.get(row.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
223
+ return { axis: type, score: meanPlayerScore(values), samples: values.length };
224
+ });
225
+ const sessionPlayerById = new Map((sessions ?? []).map((row) => [row.session_id, row.player_id]));
226
+ const squadActions = (actions ?? []).filter((action) => {
227
+ const ownerId = sessionPlayerById.get(action.session_id);
228
+ return Boolean(ownerId) && ownerId !== playerId.data;
229
+ });
230
+ const squadByAction = [...new Set(squadActions.map((action) => action.type))].map((type) => {
231
+ const matching = squadActions.filter((action) => action.type === type);
232
+ const values = matching.map((action) => scoreMap.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
233
+ return { axis: type, score: meanPlayerScore(values), samples: values.length };
234
  });
235
  const player = overview?.find((row) => row.player_id === playerId.data);
236
  if (!player) return res.status(404).json({ error: "Player not found." });
237
  const sessionsForPlayer = ownSessions.map((row) => {
238
  const sessionActions = ownActions.filter((action) => action.session_id === row.session_id);
239
  const values = sessionActions.map((action) => scoreMap.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
240
+ return { sessionId: row.session_id, createdAt: Date.parse(row.created_at) || 0, score: meanPlayerScore(values), actionCount: sessionActions.length, scoredCount: values.length };
241
  });
242
  const { data: peers, error: peersError } = await supabaseAdmin.from("players").select("player_id");
243
  if (peersError) throw new Error(`Unable to load roster: ${peersError.message}`);
244
+ const playerScores = new Map<string, number[]>();
245
+ for (const action of actions ?? []) {
246
+ const ownerId = sessionPlayerById.get(action.session_id);
247
+ const score = scoreMap.get(action.action_id);
248
+ if (ownerId && score != null && Number.isFinite(score)) playerScores.set(ownerId, [...(playerScores.get(ownerId) ?? []), score]);
249
+ }
250
+ const rankedPlayers = [...playerScores.entries()]
251
+ .map(([id, values]) => ({ id, score: meanPlayerScore(values)! }))
252
+ .sort((a, b) => b.score - a.score);
253
+ const rankIndex = rankedPlayers.findIndex((entry) => entry.id === playerId.data);
254
+ const squadAverage = { byQuality: [], byAction: squadByAction };
255
  return res.json({
256
  playerId: player.player_id, name: [player.fname, player.lname].filter(Boolean).join(" "),
257
  age: player.bdate ? String(Math.max(0, new Date().getFullYear() - new Date(player.bdate).getFullYear())) : "",
 
259
  club: player.club ?? "", position: player.position ?? "", dominantFoot: "", notes: "", avatarPath: "",
260
  createdAt: "", updatedAt: "", hasProfile: true, sessionCount: ownSessions.length, actionCount: ownActions.length,
261
  lastSessionAt: sessionsForPlayer[0]?.createdAt ?? 0,
262
+ scores: { playerId: player.player_id, rubricVersion: "database", totalActions: ownActions.length, scoredActions: ownScores.length, byAction, byQuality: [], overall: meanPlayerScore(ownScores), sessions: sessionsForPlayer },
263
+ squadAverage, rank: rankIndex < 0 ? null : rankIndex + 1, playerCount: peers?.length ?? 0,
264
  });
265
  } catch (error) { next(error); }
266
  });