brackt/app/models/participant-season-result.ts

245 lines
6.1 KiB
TypeScript
Raw Normal View History

import { database } from "~/database/context";
import * as schema from "~/database/schema";
Add oxlint linting setup with zero errors (#194) * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
import { eq, and, inArray } from "drizzle-orm";
export interface UpsertSeasonResultData {
participantId: string;
sportsSeasonId: string;
currentPoints?: number;
currentPosition?: number;
}
export interface UpdateSeasonResultData {
currentPoints?: number;
currentPosition?: number;
}
/**
* Upsert a participant's season result (for F1, etc.)
* Creates if doesn't exist, updates if it does
*/
export async function upsertParticipantSeasonResult(
data: UpsertSeasonResultData,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
// Check if result exists
const existing = await db.query.participantSeasonResults.findFirst({
where: and(
eq(schema.participantSeasonResults.participantId, data.participantId),
eq(schema.participantSeasonResults.sportsSeasonId, data.sportsSeasonId)
),
});
if (existing) {
// Update existing
const [updated] = await db
.update(schema.participantSeasonResults)
.set({
currentPoints: data.currentPoints?.toString(),
currentPosition: data.currentPosition,
updatedAt: new Date(),
})
.where(eq(schema.participantSeasonResults.id, existing.id))
.returning();
return updated;
} else {
// Insert new
const [created] = await db
.insert(schema.participantSeasonResults)
.values({
participantId: data.participantId,
sportsSeasonId: data.sportsSeasonId,
currentPoints: data.currentPoints?.toString() || "0",
currentPosition: data.currentPosition,
})
.returning();
return created;
}
}
/**
* Bulk upsert season results for multiple participants
* Useful for entering standings for an entire F1 grid at once
*/
export async function upsertSeasonResultsBulk(
results: UpsertSeasonResultData[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const upserted = [];
for (const data of results) {
const result = await upsertParticipantSeasonResult(data, db);
upserted.push(result);
}
return upserted;
}
/**
* Get a participant's season result
*/
export async function getParticipantSeasonResult(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
return await db.query.participantSeasonResults.findFirst({
where: and(
eq(schema.participantSeasonResults.participantId, participantId),
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)
),
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
}
/**
* Get all season results for a sports season
* Returns participants ordered by current position (or points if position not set)
*/
export async function getSeasonResults(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const results = await db.query.participantSeasonResults.findMany({
where: eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId),
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
// Sort by position (nulls last), then by points descending
Add oxlint linting setup with zero errors (#194) * Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00
return results.toSorted((a, b) => {
if (a.currentPosition !== null && b.currentPosition !== null) {
return a.currentPosition - b.currentPosition;
}
if (a.currentPosition !== null) return -1;
if (b.currentPosition !== null) return 1;
// Both null positions, sort by points
const aPoints = parseFloat(a.currentPoints || "0");
const bPoints = parseFloat(b.currentPoints || "0");
return bPoints - aPoints;
});
}
/**
* Get season results for specific participants
*/
export async function getSeasonResultsForParticipants(
sportsSeasonId: string,
participantIds: string[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
if (participantIds.length === 0) return [];
return await db.query.participantSeasonResults.findMany({
where: and(
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId),
inArray(schema.participantSeasonResults.participantId, participantIds)
),
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
}
/**
* Update a participant's season result
*/
export async function updateParticipantSeasonResult(
participantId: string,
sportsSeasonId: string,
data: UpdateSeasonResultData,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const [updated] = await db
.update(schema.participantSeasonResults)
.set({
currentPoints: data.currentPoints?.toString(),
currentPosition: data.currentPosition,
updatedAt: new Date(),
})
.where(
and(
eq(schema.participantSeasonResults.participantId, participantId),
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)
)
)
.returning();
return updated;
}
/**
* Delete all season results for a sports season
*/
export async function deleteSeasonResults(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
await db
.delete(schema.participantSeasonResults)
.where(eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId));
}
/**
* Check if a participant has a season result
*/
export async function hasParticipantSeasonResult(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
): Promise<boolean> {
const db = providedDb || database();
const result = await db.query.participantSeasonResults.findFirst({
where: and(
eq(schema.participantSeasonResults.participantId, participantId),
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)
),
});
return !!result;
}