Adds live standings sync and display for bracket-based sports (NBA/NHL), so league members can see W/L tables and which teams their opponents drafted during the regular season — not just after the playoff bracket is set. - New `regular_season_standings` table with upsert-on-conflict sync - Standings sync service with NHL (api-web.nhle.com) and NBA (ESPN) adapters, externalId write-back for future syncs, and unmatched-team resolution UI - `RegularSeasonStandings` component: flat (NBA) + division/wild-card (NHL) modes, playoff line, TeamOwnerBadge, projected Brackt points (EV), mobile horizontal scroll - Admin "Sync Standings" card + "Resolve Unmatched" UI on sports season page - Admin manual standings edit hatch at /admin/sports-seasons/:id/regular-standings - Show standings above bracket until matches exist; below once bracket is set - `normalize-team-name` utility extracted to shared lib Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
import { eq, and } from "drizzle-orm";
|
|
import type { FetchedStandingsRecord } from "~/services/standings-sync/types";
|
|
|
|
/**
|
|
* Upsert a pending mapping for an unmatched team from a standings sync.
|
|
* Uses onConflictDoUpdate so re-syncing refreshes the standing data.
|
|
*/
|
|
export async function upsertPendingStandingsMapping(
|
|
sportsSeasonId: string,
|
|
record: FetchedStandingsRecord,
|
|
providedDb?: ReturnType<typeof database>
|
|
) {
|
|
const db = providedDb || database();
|
|
|
|
const [row] = await db
|
|
.insert(schema.pendingStandingsMappings)
|
|
.values({
|
|
sportsSeasonId,
|
|
externalTeamId: record.externalTeamId,
|
|
teamName: record.teamName,
|
|
standingData: record as unknown as Record<string, unknown>,
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: [
|
|
schema.pendingStandingsMappings.sportsSeasonId,
|
|
schema.pendingStandingsMappings.externalTeamId,
|
|
],
|
|
set: {
|
|
teamName: record.teamName,
|
|
standingData: record as unknown as Record<string, unknown>,
|
|
},
|
|
})
|
|
.returning();
|
|
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* Bulk upsert pending mappings for all unmatched teams in a sync.
|
|
*/
|
|
export async function upsertPendingStandingsMappings(
|
|
sportsSeasonId: string,
|
|
records: FetchedStandingsRecord[],
|
|
providedDb?: ReturnType<typeof database>
|
|
) {
|
|
const db = providedDb || database();
|
|
return Promise.all(records.map((r) => upsertPendingStandingsMapping(sportsSeasonId, r, db)));
|
|
}
|
|
|
|
/**
|
|
* Get all pending mappings for a sports season (awaiting admin resolution).
|
|
*/
|
|
export async function getPendingStandingsMappings(
|
|
sportsSeasonId: string,
|
|
providedDb?: ReturnType<typeof database>
|
|
) {
|
|
const db = providedDb || database();
|
|
|
|
return db.query.pendingStandingsMappings.findMany({
|
|
where: eq(schema.pendingStandingsMappings.sportsSeasonId, sportsSeasonId),
|
|
orderBy: schema.pendingStandingsMappings.teamName,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Delete a resolved pending mapping by its externalTeamId.
|
|
*/
|
|
export async function deletePendingStandingsMapping(
|
|
sportsSeasonId: string,
|
|
externalTeamId: string,
|
|
providedDb?: ReturnType<typeof database>
|
|
) {
|
|
const db = providedDb || database();
|
|
|
|
await db
|
|
.delete(schema.pendingStandingsMappings)
|
|
.where(
|
|
and(
|
|
eq(schema.pendingStandingsMappings.sportsSeasonId, sportsSeasonId),
|
|
eq(schema.pendingStandingsMappings.externalTeamId, externalTeamId)
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete all pending mappings for a sports season.
|
|
* Used when manually clearing the queue.
|
|
*/
|
|
export async function deleteAllPendingStandingsMappings(
|
|
sportsSeasonId: string,
|
|
providedDb?: ReturnType<typeof database>
|
|
) {
|
|
const db = providedDb || database();
|
|
|
|
await db
|
|
.delete(schema.pendingStandingsMappings)
|
|
.where(eq(schema.pendingStandingsMappings.sportsSeasonId, sportsSeasonId));
|
|
}
|