Spaces:
Running
Running
Normalize player ratings to a 100-point scale
Browse files- 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:
|
| 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:
|
| 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:
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
});
|