{
);
};
-const PlayerStatsTableSkeleton = () => {
+interface PlayerStatsTableSkeletonProps {
+ hideFilters?: boolean;
+}
+
+const PlayerStatsTableSkeleton = ({ hideFilters = false }: PlayerStatsTableSkeletonProps) => {
return (
diff --git a/src/features/players/components/player-stats-table.tsx b/src/features/players/components/player-stats-table.tsx
index e602c08..f2b9fba 100644
--- a/src/features/players/components/player-stats-table.tsx
+++ b/src/features/players/components/player-stats-table.tsx
@@ -142,8 +142,12 @@ const PlayerListItem = memo(({ stat, onPlayerClick, mmr, onRegisterViewport, onU
);
});
-const PlayerStatsTable = () => {
- const { data: playerStats } = useAllPlayerStats();
+interface PlayerStatsTableProps {
+ viewType?: 'all' | 'mainline' | 'regional';
+}
+
+const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
+ const { data: playerStats } = useAllPlayerStats(viewType);
const navigate = useNavigate();
const [search, setSearch] = useState("");
const [sortConfig, setSortConfig] = useState({
@@ -292,21 +296,19 @@ const PlayerStatsTable = () => {
if (playerStats.length === 0) {
return (
-
-
-
-
-
-
- No Stats Available
-
-
-
+
+
+
+
+
+ No Stats Available
+
+
);
}
return (
-
+
Showing {filteredAndSortedStats.length} of {playerStats.length} players
diff --git a/src/features/players/components/profile/index.tsx b/src/features/players/components/profile/index.tsx
index 33c397d..ff524bb 100644
--- a/src/features/players/components/profile/index.tsx
+++ b/src/features/players/components/profile/index.tsx
@@ -1,10 +1,10 @@
-import { Box, Stack, Text, Divider } from "@mantine/core";
-import { Suspense } from "react";
+import { Box, Stack, Text, Divider, Group, Button } from "@mantine/core";
+import { Suspense, useState, useDeferredValue } from "react";
import Header from "./header";
import SwipeableTabs from "@/components/swipeable-tabs";
import { usePlayer, usePlayerMatches, usePlayerStats } from "../../queries";
import TeamList from "@/features/teams/components/team-list";
-import StatsOverview from "@/components/stats-overview";
+import StatsOverview, { StatsSkeleton } from "@/components/stats-overview";
import MatchList from "@/features/matches/components/match-list";
import BadgeShowcase from "@/features/badges/components/badge-showcase";
import BadgeShowcaseSkeleton from "@/features/badges/components/badge-showcase-skeleton";
@@ -13,10 +13,38 @@ interface ProfileProps {
id: string;
}
+const StatsWithFilter = ({ id }: { id: string }) => {
+ const [viewType, setViewType] = useState<'all' | 'mainline' | 'regional'>('all');
+ const deferredViewType = useDeferredValue(viewType);
+ const isStale = viewType !== deferredViewType;
+
+ return (
+
+
+ Statistics
+
+
+
+
+
+
+
+ }>
+
+
+
+
+ );
+};
+
+const StatsContent = ({ id, viewType }: { id: string; viewType: 'all' | 'mainline' | 'regional' }) => {
+ const { data: stats, isLoading: statsLoading } = usePlayerStats(id, viewType);
+ return ;
+};
+
const Profile = ({ id }: ProfileProps) => {
const { data: player } = usePlayer(id);
const { data: matches } = usePlayerMatches(id);
- const { data: stats, isLoading: statsLoading } = usePlayerStats(id);
const tabs = [
{
@@ -29,10 +57,7 @@ const Profile = ({ id }: ProfileProps) => {
-
- Statistics
-
-
+
>,
},
{
diff --git a/src/features/players/queries.ts b/src/features/players/queries.ts
index 3a871b4..eb61288 100644
--- a/src/features/players/queries.ts
+++ b/src/features/players/queries.ts
@@ -7,8 +7,8 @@ export const playerKeys = {
details: (id: string) => ['players', 'details', id],
unassociated: ['players','unassociated'],
unenrolled: (tournamentId: string) => ['players', 'unenrolled', tournamentId],
- stats: (id: string) => ['players', 'stats', id],
- allStats: ['players', 'stats', 'all'],
+ stats: (id: string, viewType?: 'all' | 'mainline' | 'regional') => ['players', 'stats', id, viewType ?? 'all'],
+ allStats: (viewType?: 'all' | 'mainline' | 'regional') => ['players', 'stats', 'all', viewType ?? 'all'],
matches: (id: string) => ['players', 'matches', id],
activity: ['players', 'activity'],
};
@@ -34,13 +34,13 @@ export const playerQueries = {
queryKey: playerKeys.unenrolled(tournamentId),
queryFn: async () => await getUnenrolledPlayers({ data: tournamentId })
}),
- stats: (id: string) => ({
- queryKey: playerKeys.stats(id),
- queryFn: async () => await getPlayerStats({ data: id })
+ stats: (id: string, viewType?: 'all' | 'mainline' | 'regional') => ({
+ queryKey: playerKeys.stats(id, viewType),
+ queryFn: async () => await getPlayerStats({ data: { playerId: id, viewType } })
}),
- allStats: () => ({
- queryKey: playerKeys.allStats,
- queryFn: async () => await getAllPlayerStats()
+ allStats: (viewType?: 'all' | 'mainline' | 'regional') => ({
+ queryKey: playerKeys.allStats(viewType),
+ queryFn: async () => await getAllPlayerStats({ data: viewType })
}),
matches: (id: string) => ({
queryKey: playerKeys.matches(id),
@@ -84,11 +84,11 @@ export const usePlayers = () =>
export const useUnassociatedPlayers = () =>
useServerSuspenseQuery(playerQueries.unassociated());
-export const usePlayerStats = (id: string) =>
- useServerSuspenseQuery(playerQueries.stats(id));
+export const usePlayerStats = (id: string, viewType?: 'all' | 'mainline' | 'regional') =>
+ useServerSuspenseQuery(playerQueries.stats(id, viewType));
-export const useAllPlayerStats = () =>
- useServerSuspenseQuery(playerQueries.allStats());
+export const useAllPlayerStats = (viewType?: 'all' | 'mainline' | 'regional') =>
+ useServerSuspenseQuery(playerQueries.allStats(viewType));
export const usePlayerMatches = (id: string) =>
useServerSuspenseQuery(playerQueries.matches(id));
diff --git a/src/features/players/server.ts b/src/features/players/server.ts
index 399dba3..56874e1 100644
--- a/src/features/players/server.ts
+++ b/src/features/players/server.ts
@@ -136,16 +136,20 @@ export const getUnassociatedPlayers = createServerFn()
);
export const getPlayerStats = createServerFn()
- .inputValidator(z.string())
+ .inputValidator(z.object({
+ playerId: z.string(),
+ viewType: z.enum(['all', 'mainline', 'regional']).optional()
+ }))
.middleware([superTokensFunctionMiddleware])
.handler(async ({ data }) =>
- toServerResult(async () => await pbAdmin.getPlayerStats(data))
+ toServerResult(async () => await pbAdmin.getPlayerStats(data.playerId, data.viewType))
);
export const getAllPlayerStats = createServerFn()
+ .inputValidator(z.enum(['all', 'mainline', 'regional']).optional())
.middleware([superTokensFunctionMiddleware])
- .handler(async () =>
- toServerResult(async () => await pbAdmin.getAllPlayerStats())
+ .handler(async ({ data }) =>
+ toServerResult(async () => await pbAdmin.getAllPlayerStats(data))
);
export const getPlayerMatches = createServerFn()
diff --git a/src/features/teams/components/team-list.tsx b/src/features/teams/components/team-list.tsx
index 0812a84..2c006e0 100644
--- a/src/features/teams/components/team-list.tsx
+++ b/src/features/teams/components/team-list.tsx
@@ -55,10 +55,10 @@ const TeamList = ({ teams, loading = false, onTeamClick }: TeamListProps) => {
const navigate = useNavigate();
const handleClick = useCallback(
- (teamId: string) => {
+ (teamId: string, priv: boolean) => {
if (onTeamClick) {
onTeamClick(teamId);
- } else {
+ } else if (!priv) {
navigate({ to: `/teams/${teamId}` });
}
},
@@ -100,7 +100,7 @@ const TeamList = ({ teams, loading = false, onTeamClick }: TeamListProps) => {
/>
}
style={{ cursor: "pointer" }}
- onClick={() => handleClick(team.id)}
+ onClick={() => handleClick(team.id, team.private)}
styles={{
itemWrapper: { width: "100%" },
itemLabel: { width: "100%" },
diff --git a/src/features/teams/types.ts b/src/features/teams/types.ts
index c034a53..4b7cafa 100644
--- a/src/features/teams/types.ts
+++ b/src/features/teams/types.ts
@@ -19,6 +19,7 @@ export interface Team {
updated: string;
players: PlayerInfo[];
tournaments: TournamentInfo[];
+ private: boolean;
}
export interface TeamInfo {
@@ -28,6 +29,7 @@ export interface TeamInfo {
accent_color: string;
logo?: string;
players: PlayerInfo[];
+ private: boolean;
}
export const teamInputSchema = z
diff --git a/src/features/tournaments/components/tournament-card.tsx b/src/features/tournaments/components/tournament-card.tsx
index 2aebe0d..88c864d 100644
--- a/src/features/tournaments/components/tournament-card.tsx
+++ b/src/features/tournaments/components/tournament-card.tsx
@@ -58,7 +58,7 @@ export const TournamentCard = ({ tournament }: TournamentCardProps) => {
{tournament.name}
- {(tournament.first_place || tournament.second_place || tournament.third_place) && (
+ {((tournament.first_place || tournament.second_place || tournament.third_place) && !tournament.regional) && (
{tournament.first_place && (
{
return (
-
+ {tournament.regional && (
+ }>
+ Regional tournaments are a work in progress. Some features might not work as expected.
+
+ )}
+ {!tournament.regional && }
{
const criteria = badge.criteria;
- const stats = await pb.collection("player_stats").getFirstListItem(
+ const stats = await pb.collection("player_mainline_stats").getFirstListItem(
`player_id = "${playerId}"`
).catch(() => null);
@@ -103,8 +103,8 @@ export function createBadgesService(pb: PocketBase) {
if (criteria.overtime_matches !== undefined || criteria.overtime_wins !== undefined) {
const matches = await pb.collection("matches").getFullList({
- filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && ot_count > 0`,
- expand: 'home,away,home.players,away.players',
+ filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && ot_count > 0 && (tournament.regional = false || tournament.regional = null)`,
+ expand: 'tournament,home,away,home.players,away.players',
});
if (criteria.overtime_matches !== undefined) {
@@ -131,8 +131,8 @@ export function createBadgesService(pb: PocketBase) {
if (criteria.margin_of_victory !== undefined) {
const matches = await pb.collection("matches").getFullList({
- filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended"`,
- expand: 'home,away,home.players,away.players',
+ filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && (tournament.regional = false || tournament.regional = null)`,
+ expand: 'tournament,home,away,home.players,away.players',
});
const bigWins = matches.filter(m => {
@@ -159,7 +159,7 @@ export function createBadgesService(pb: PocketBase) {
const criteria = badge.criteria;
const matches = await pb.collection("matches").getFullList({
- filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended"`,
+ filter: `(home.players.id ?~ "${playerId}" || away.players.id ?~ "${playerId}") && status = "ended" && (tournament.regional = false || tournament.regional = null)`,
expand: 'tournament,home,away,home.players,away.players',
});
@@ -209,8 +209,8 @@ export function createBadgesService(pb: PocketBase) {
for (const tournamentId of tournamentIds) {
const tournamentMatches = await pb.collection("matches").getFullList({
- filter: `tournament = "${tournamentId}" && status = "ended"`,
- expand: 'home,away,home.players,away.players',
+ filter: `tournament = "${tournamentId}" && status = "ended" && (tournament.regional = false || tournament.regional = null)`,
+ expand: 'tournament,home,away,home.players,away.players',
});
const winnersMatches = tournamentMatches.filter(m => !m.is_losers_bracket);
@@ -241,8 +241,8 @@ export function createBadgesService(pb: PocketBase) {
for (const tournamentId of tournamentIds) {
const tournamentMatches = await pb.collection("matches").getFullList({
- filter: `tournament = "${tournamentId}" && status = "ended"`,
- expand: 'home,away,home.players,away.players',
+ filter: `tournament = "${tournamentId}" && status = "ended" && (tournament.regional = false || tournament.regional = null)`,
+ expand: 'tournament,home,away,home.players,away.players',
});
if (criteria.placement === 2) {
@@ -293,6 +293,7 @@ export function createBadgesService(pb: PocketBase) {
if (criteria.tournament_record !== undefined) {
const tournaments = await pb.collection("tournaments").getFullList({
+ filter: 'regional = false || regional = null',
sort: 'start_time',
});
@@ -344,6 +345,7 @@ export function createBadgesService(pb: PocketBase) {
if (criteria.consecutive_wins !== undefined) {
const tournaments = await pb.collection("tournaments").getFullList({
+ filter: 'regional = false || regional = null',
sort: 'start_time',
});
diff --git a/src/lib/pocketbase/services/matches.ts b/src/lib/pocketbase/services/matches.ts
index 8ce3943..9bcb325 100644
--- a/src/lib/pocketbase/services/matches.ts
+++ b/src/lib/pocketbase/services/matches.ts
@@ -32,7 +32,7 @@ export function createMatchesService(pb: PocketBase) {
},
async createMatch(data: MatchInput): Promise {
- logger.info("PocketBase | Creating match", data);
+ // logger.info("PocketBase | Creating match", data);
const result = await pb.collection("matches").create(data);
return result;
},
@@ -92,23 +92,40 @@ export function createMatchesService(pb: PocketBase) {
return [];
}
- const filterConditions: string[] = [];
- player1TeamIds.forEach(team1Id => {
- player2TeamIds.forEach(team2Id => {
- filterConditions.push(`(home="${team1Id}" && away="${team2Id}")`);
- filterConditions.push(`(home="${team2Id}" && away="${team1Id}")`);
+ const allTeamIds = [...new Set([...player1TeamIds, ...player2TeamIds])];
+ const batchSize = 10;
+ const allMatches: any[] = [];
+
+ for (let i = 0; i < allTeamIds.length; i += batchSize) {
+ const batch = allTeamIds.slice(i, i + batchSize);
+ const teamFilters = batch.map(id => `home="${id}" || away="${id}"`).join(' || ');
+
+ const results = await pb.collection("matches").getFullList({
+ filter: teamFilters,
+ expand: "tournament, home, away, home.players, away.players",
+ sort: "-created",
});
- });
- const filter = filterConditions.join(" || ");
+ allMatches.push(...results);
+ }
- const results = await pb.collection("matches").getFullList({
- filter,
- expand: "tournament, home, away, home.players, away.players",
- sort: "-created",
- });
+ const uniqueMatches = Array.from(
+ new Map(allMatches.map(m => [m.id, m])).values()
+ );
- return results.map(match => transformMatch(match));
+ return uniqueMatches
+ .filter(match => {
+ const homeTeamId = typeof match.home === 'string' ? match.home : match.home?.id;
+ const awayTeamId = typeof match.away === 'string' ? match.away : match.away?.id;
+
+ const player1InHome = player1TeamIds.includes(homeTeamId);
+ const player1InAway = player1TeamIds.includes(awayTeamId);
+ const player2InHome = player2TeamIds.includes(homeTeamId);
+ const player2InAway = player2TeamIds.includes(awayTeamId);
+
+ return (player1InHome && player2InAway) || (player1InAway && player2InHome);
+ })
+ .map(match => transformMatch(match));
},
async getMatchesBetweenTeams(team1Id: string, team2Id: string): Promise {
diff --git a/src/lib/pocketbase/services/players.ts b/src/lib/pocketbase/services/players.ts
index 89a720d..2d4c638 100644
--- a/src/lib/pocketbase/services/players.ts
+++ b/src/lib/pocketbase/services/players.ts
@@ -8,7 +8,6 @@ import type {
import type { Match } from "@/features/matches/types";
import { transformPlayer, transformPlayerInfo, transformMatch } from "@/lib/pocketbase/util/transform-types";
import PocketBase from "pocketbase";
-import { DataFetchOptions } from "./base";
export function createPlayersService(pb: PocketBase) {
return {
@@ -65,9 +64,15 @@ export function createPlayersService(pb: PocketBase) {
return result.map(transformPlayer);
},
- async getPlayerStats(playerId: string): Promise {
+ async getPlayerStats(playerId: string, viewType: 'all' | 'mainline' | 'regional' = 'all'): Promise {
try {
- const result = await pb.collection("player_stats").getFirstListItem(
+ const collectionMap = {
+ all: 'player_stats',
+ mainline: 'player_mainline_stats',
+ regional: 'player_regional_stats',
+ };
+
+ const result = await pb.collection(collectionMap[viewType]).getFirstListItem(
`player_id = "${playerId}"`
);
return result;
@@ -90,8 +95,14 @@ export function createPlayersService(pb: PocketBase) {
}
},
- async getAllPlayerStats(): Promise {
- const result = await pb.collection("player_stats").getFullList({
+ async getAllPlayerStats(viewType: 'all' | 'mainline' | 'regional' = 'all'): Promise {
+ const collectionMap = {
+ all: 'player_stats',
+ mainline: 'player_mainline_stats',
+ regional: 'player_regional_stats',
+ };
+
+ const result = await pb.collection(collectionMap[viewType]).getFullList({
sort: "-win_percentage,-total_cups_made",
});
return result;
diff --git a/src/lib/pocketbase/services/tournaments.ts b/src/lib/pocketbase/services/tournaments.ts
index fe483dd..592a0e3 100644
--- a/src/lib/pocketbase/services/tournaments.ts
+++ b/src/lib/pocketbase/services/tournaments.ts
@@ -34,7 +34,7 @@ export function createTournamentsService(pb: PocketBase) {
.getFirstListItem('',
{
expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away, matches.home.players, matches.away.players",
- sort: "-created",
+ sort: "-start_time",
}
);
@@ -52,7 +52,7 @@ export function createTournamentsService(pb: PocketBase) {
.collection("tournaments")
.getFullList({
expand: "teams,teams.players,matches",
- sort: "-created",
+ sort: "-start_time",
});
const tournamentsWithStats = await Promise.all(result.map(async (tournament) => {
diff --git a/src/lib/pocketbase/util/transform-types.ts b/src/lib/pocketbase/util/transform-types.ts
index 03c583d..900de8f 100644
--- a/src/lib/pocketbase/util/transform-types.ts
+++ b/src/lib/pocketbase/util/transform-types.ts
@@ -1,4 +1,3 @@
-import { Reaction } from "@/features/matches/server";
import { Match } from "@/features/matches/types";
import { Player, PlayerInfo } from "@/features/players/types";
import { Team, TeamInfo } from "@/features/teams/types";
@@ -25,7 +24,8 @@ export function transformTeamInfo(record: any): TeamInfo {
primary_color: record.primary_color,
accent_color: record.accent_color,
players,
- logo: record.logo
+ logo: record.logo,
+ private: record.private || false,
};
}
@@ -107,6 +107,7 @@ export const transformTournamentInfo = (record: any): TournamentInfo => {
end_time: record.end_time,
logo: record.logo,
glitch_logo: record.glitch_logo,
+ regional: record.regional || false,
first_place,
second_place,
third_place,
@@ -116,6 +117,7 @@ export const transformTournamentInfo = (record: any): TournamentInfo => {
export function transformPlayer(record: any): Player {
const teams =
record.expand?.teams
+ ?.filter((team: any) => !team.private)
?.sort((a: any, b: any) =>
new Date(a.created) < new Date(b.created) ? -1 : 0
)
@@ -135,13 +137,6 @@ export function transformPlayer(record: any): Player {
export function transformFreeAgent(record: any) {
const player = record.expand?.player ? transformPlayerInfo(record.expand.player) : undefined;
- const tournaments =
- record.expand?.tournaments
- ?.sort((a: any, b: any) =>
- new Date(a.created!) < new Date(b.created!) ? -1 : 0
- )
- ?.map(transformTournamentInfo) ?? [];
-
return {
id: record.id as string,
phone: record.phone as string,
@@ -180,6 +175,7 @@ export function transformTeam(record: any): Team {
updated: record.updated,
players,
tournaments,
+ private: record.private || false,
};
}
@@ -264,6 +260,7 @@ export function transformTournament(record: any, isAdmin: boolean = false): Tour
end_time: record.end_time,
created: record.created,
updated: record.updated,
+ regional: record.regional || false,
teams,
matches,
first_place,
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..739a63c
--- /dev/null
+++ b/test.js
@@ -0,0 +1,269 @@
+import PocketBase from "pocketbase";
+import * as xlsx from "xlsx";
+import { nanoid } from "nanoid";
+
+import { createTeamsService } from "./src/lib/pocketbase/services/teams.ts";
+import { createPlayersService } from "./src/lib/pocketbase/services/players.ts";
+import { createMatchesService } from "./src/lib/pocketbase/services/matches.ts";
+import { createTournamentsService } from "./src/lib/pocketbase/services/tournaments.ts";
+
+const POCKETBASE_URL = "http://127.0.0.1:8090";
+const EXCEL_FILE_PATH = "./Teams-2.xlsx";
+
+const ADMIN_EMAIL = "kyle.yohler@gmail.com";
+const ADMIN_PASSWORD = "xj44aqz9CWrNNM0o";
+
+// --- Helpers ---
+async function createPlayerIfMissing(playersService, nameColumn, idColumn) {
+ const playerId = idColumn?.trim();
+ if (playerId) return playerId;
+
+ let firstName, lastName;
+ if (!nameColumn || !nameColumn.trim()) {
+ firstName = `Player_${nanoid(4)}`;
+ lastName = "(Regional)";
+ } else {
+ const parts = nameColumn.trim().split(" ");
+ firstName = parts[0];
+ lastName = parts[1] || "(Regional)";
+ }
+
+ const newPlayer = await playersService.createPlayer({ first_name: firstName, last_name: lastName });
+ return newPlayer.id;
+}
+
+async function handleTeamsSheet(rows, teamsService, playersService, pb, tournamentIdMap = {}) {
+ console.log(`š„ Importing ${rows.length} teams...`);
+ const teamIdMap = {}; // spreadsheet ID -> PocketBase ID
+
+ for (const [i, row] of rows.entries()) {
+ try {
+ const spreadsheetTeamId = row["ID"]?.toString().trim();
+ if (!spreadsheetTeamId) {
+ console.warn(`ā ļø [${i + 1}] Team row missing spreadsheet ID, skipping.`);
+ continue;
+ }
+
+ const p1Id = await createPlayerIfMissing(playersService, row["P1 Name"], row["P1 ID"]);
+ const p2Id = await createPlayerIfMissing(playersService, row["P2 Name"], row["P2 ID"]);
+
+ let name = row["Name"]?.trim();
+ if (!name) {
+ const p1First = row["P1 Name"]?.split(" ")[0] || "Player1";
+ const p2First = row["P2 Name"]?.split(" ")[0] || "Player2";
+ name = `${p1First} and ${p2First}`;
+ console.warn(`ā ļø [${i + 1}] No team name found. Using generated name: ${name}`);
+ }
+
+ const existing = await pb.collection("teams").getFullList({
+ filter: `name = "${name}"`,
+ fields: "id",
+ });
+
+ if (existing.length > 0) {
+ console.log(`ā¹ļø [${i + 1}] Team "${name}" already exists, skipping.`);
+ teamIdMap[spreadsheetTeamId] = existing[0].id;
+ continue;
+ }
+
+ // If there's a tournament for this team, get its PB ID
+ const tournamentSpreadsheetId = row["Tournament ID"]?.toString().trim();
+ const tournamentId = tournamentSpreadsheetId ? tournamentIdMap[tournamentSpreadsheetId] : undefined;
+
+ const teamInput = {
+ name,
+ primary_color: row.primary_color || "",
+ accent_color: row.accent_color || "",
+ logo: row.logo || "",
+ players: [p1Id, p2Id],
+ tournament: tournamentId, // single tournament relation,
+ private: true
+ };
+
+ const team = await teamsService.createTeam(teamInput);
+ teamIdMap[spreadsheetTeamId] = team.id;
+
+ console.log(`ā
[${i + 1}] Created team: ${team.name} with players: ${[p1Id, p2Id].join(", ")}`);
+
+ // Add the team to the tournament's "teams" relation
+ if (tournamentId) {
+ await pb.collection("tournaments").update(tournamentId, {
+ "teams+": [team.id],
+ });
+ console.log(`ā
Added team "${team.name}" to tournament ${tournamentId}`);
+ }
+ } catch (err) {
+ console.error(`ā [${i + 1}] Failed to create team: ${err.message}`);
+ }
+ }
+
+ return teamIdMap;
+}
+
+
+async function handleTournamentSheet(rows, tournamentsService, teamIdMap, pb) {
+ console.log(`š„ Importing ${rows.length} tournaments...`);
+ const tournamentIdMap = {};
+ const validFormats = ["double_elim", "single_elim", "groups", "swiss", "swiss_bracket"];
+
+ for (const [i, row] of rows.entries()) {
+ try {
+ const spreadsheetId = row["ID"]?.toString().trim();
+ if (!spreadsheetId) {
+ console.warn(`ā ļø [${i + 1}] Tournament missing spreadsheet ID, skipping.`);
+ continue;
+ }
+
+ if (!row["Name"]) {
+ console.warn(`ā ļø [${i + 1}] Tournament name missing, skipping.`);
+ continue;
+ }
+
+ const format = validFormats.includes(row["Format"]) ? row["Format"] : "double_elim";
+
+ // Convert start_time to ISO datetime string
+ let startTime = null;
+ if (row["Start Time"]) {
+ try {
+ startTime = new Date(row["Start Time"]).toISOString();
+ } catch (e) {
+ console.warn(`ā ļø [${i + 1}] Invalid start time format, using null`);
+ }
+ }
+
+ const tournamentInput = {
+ name: row["Name"],
+ start_time: startTime,
+ format,
+ regional: true,
+ teams: Object.values(teamIdMap), // Add all created teams
+ };
+
+ const tournament = await tournamentsService.createTournament(tournamentInput);
+ tournamentIdMap[spreadsheetId] = tournament.id;
+
+ console.log(`ā
[${i + 1}] Created tournament: ${tournament.name} with ${Object.values(teamIdMap).length} teams`);
+ } catch (err) {
+ console.error(`ā [${i + 1}] Failed to create tournament: ${err.message}`);
+ }
+ }
+
+ return tournamentIdMap;
+}
+
+
+async function handleMatchesSheet(rows, matchesService, teamIdMap, tournamentIdMap, pb) {
+ console.log(`š„ Importing ${rows.length} matches...`);
+
+ const tournamentMatchesMap = {};
+
+ for (const [i, row] of rows.entries()) {
+ try {
+ const homeId = teamIdMap[row["Home ID"]];
+ const awayId = teamIdMap[row["Away ID"]];
+ const tournamentId = tournamentIdMap[row["Tournament ID"]];
+
+ if (!homeId || !awayId || !tournamentId) {
+ console.warn(`ā ļø [${i + 1}] Could not find mapping for Home, Away, or Tournament, skipping.`);
+ continue;
+ }
+
+ // --- Ensure the teams are linked to the tournament ---
+ for (const teamId of [homeId, awayId]) {
+ const team = await pb.collection("teams").getOne(teamId, { fields: "tournaments" });
+ const tournaments = team.tournaments || [];
+ if (!tournaments.includes(tournamentId)) {
+ // Add tournament to team
+ await pb.collection("teams").update(teamId, { "tournaments+": [tournamentId] });
+ // Add team to tournament
+ await pb.collection("tournaments").update(tournamentId, { "teams+": [teamId] });
+ console.log(`ā
Linked team ${team.name} to tournament ${tournamentId}`);
+ }
+ }
+
+ // --- Create match ---
+ const data = {
+ tournament: tournamentId,
+ home: homeId,
+ away: awayId,
+ home_cups: Number(row["Home cups"] || 0),
+ away_cups: Number(row["Away cups"] || 0),
+ status: "ended",
+ lid: i+1
+ };
+
+ const match = await matchesService.createMatch(data);
+ console.log(`ā
[${i + 1}] Created match ID: ${match.id}`);
+
+ if (!tournamentMatchesMap[tournamentId]) tournamentMatchesMap[tournamentId] = [];
+ tournamentMatchesMap[tournamentId].push(match.id);
+ } catch (err) {
+ console.error(`ā [${i + 1}] Failed to create match: ${err.message}`);
+ }
+ }
+
+ // Update each tournament with the created match IDs
+ for (const [tournamentId, matchIds] of Object.entries(tournamentMatchesMap)) {
+ try {
+ await pb.collection("tournaments").update(tournamentId, { "matches+": matchIds });
+ console.log(`ā
Updated tournament ${tournamentId} with ${matchIds.length} matches`);
+ } catch (err) {
+ console.error(`ā Failed to update tournament ${tournamentId} with matches: ${err.message}`);
+ }
+ }
+}
+
+
+// --- Main Import ---
+export async function importExcel() {
+ const pb = new PocketBase(POCKETBASE_URL);
+ await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
+
+ const teamsService = createTeamsService(pb);
+ const playersService = createPlayersService(pb);
+ const tournamentsService = createTournamentsService(pb);
+ const matchesService = createMatchesService(pb);
+
+ const workbook = xlsx.readFile(EXCEL_FILE_PATH);
+
+ let teamIdMap = {};
+ let tournamentIdMap = {};
+
+ // Process sheets in correct order: Tournaments -> Teams -> Matches
+ const sheetOrder = ["tournament", "tournaments", "teams", "matches"];
+ const processedSheets = new Set();
+
+ for (const sheetNamePattern of sheetOrder) {
+ for (const sheetName of workbook.SheetNames) {
+ if (processedSheets.has(sheetName)) continue;
+ if (sheetName.toLowerCase() !== sheetNamePattern) continue;
+
+ const worksheet = workbook.Sheets[sheetName];
+ const rows = xlsx.utils.sheet_to_json(worksheet);
+
+ console.log(`\nš Processing sheet: ${sheetName}`);
+
+ switch (sheetName.toLowerCase()) {
+ case "teams":
+ teamIdMap = await handleTeamsSheet(rows, teamsService, playersService, pb, tournamentIdMap);
+ break;
+ case "tournament":
+ case "tournaments":
+ tournamentIdMap = await handleTournamentSheet(rows, tournamentsService, teamIdMap, pb);
+ break;
+ case "matches":
+ await handleMatchesSheet(rows, matchesService, teamIdMap, tournamentIdMap, pb);
+ break;
+ default:
+ console.log(`ā ļø No handler found for sheet '${sheetName}', skipping.`);
+ }
+
+ processedSheets.add(sheetName);
+ }
+ }
+
+ console.log("\nš All sheets imported successfully!");
+}
+
+// --- Run ---
+importExcel().catch(console.error);
\ No newline at end of file