From 9d1765d8a48250ac9b70c72dd80ffd2618d38d25 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:53:40 -0700 Subject: [PATCH] Auto-name teams on claim and harden owner-assignment logic (#163) * Auto-name teams on claim and harden owner-assignment logic - Add claimTeam() model function that atomically sets ownerId + name in a single DB write, replacing the previous two-step assignTeamOwner + renameTeam pattern that could leave partial state on failure - Rename team to "Team " (fallback: displayName, then "Member") when a user joins via invite link or an admin assigns a team owner - Fetch the user record before claiming the team in both paths so we fail fast with a clear error rather than silently skipping the rename - Guard against assigning an already-owned team in the admin settings action (closes #107, closes #76) - Show owner username below team name in draft order settings UI; teams with an owner but no matching user record display "Unknown" Co-Authored-By: Claude Sonnet 4.6 * Fixing dropdown for admins --------- Co-authored-by: Claude Sonnet 4.6 --- app/models/team.ts | 8 ++++ app/routes/i.$inviteCode.tsx | 15 ++++++-- app/routes/leagues/$leagueId.settings.tsx | 45 +++++++++++------------ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/app/models/team.ts b/app/models/team.ts index 5f78e0e..3e5cf0e 100644 --- a/app/models/team.ts +++ b/app/models/team.ts @@ -83,6 +83,14 @@ export async function assignTeamOwner( return await updateTeam(id, { ownerId }); } +export async function claimTeam( + id: string, + ownerId: string, + name: string +): Promise { + return await updateTeam(id, { ownerId, name }); +} + export async function removeTeamOwner(id: string): Promise { return await updateTeam(id, { ownerId: null }); } diff --git a/app/routes/i.$inviteCode.tsx b/app/routes/i.$inviteCode.tsx index d61b610..10ae4d9 100644 --- a/app/routes/i.$inviteCode.tsx +++ b/app/routes/i.$inviteCode.tsx @@ -8,8 +8,9 @@ import { findLeagueById, findCommissionersByLeagueId, findAvailableTeams, - assignTeamOwner, + claimTeam, isUserLeagueMember, + findUserByClerkId, } from "~/models"; import { Button } from "~/components/ui/button"; import { @@ -94,8 +95,16 @@ export async function action(args: Route.ActionArgs) { throw new Response("No available teams in this league", { status: 400 }); } - // Assign the first available team to the user - await assignTeamOwner(availableTeams[0].id, userId); + // Look up user before claiming to fail fast and get their name + const user = await findUserByClerkId(userId); + if (!user) { + throw new Response("User account not found. Please try again.", { status: 500 }); + } + + // Claim the first available team, setting owner and name atomically + const team = availableTeams[0]; + const teamName = `Team ${user.username || user.displayName || "Member"}`; + await claimTeam(team.id, userId, teamName); // Redirect to league page return redirect(`/leagues/${season.leagueId}?joined=true`); diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index db0feab..c420029 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -14,7 +14,7 @@ import { removeCommissionerByLeagueAndUser, } from "~/models/commissioner"; import { findCurrentSeasonWithSports, updateSeason } from "~/models/season"; -import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, assignTeamOwner } from "~/models/team"; +import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team"; import { findAllUsers, findUserByClerkId, isUserAdminByClerkId } from "~/models/user"; import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport"; import { findAllSportsSeasons } from "~/models/sports-season"; @@ -361,16 +361,30 @@ export async function action(args: Route.ActionArgs) { return { error: "Only admins can assign team owners" }; } + // Look up user before assigning to fail fast and get their name + const assignedUser = await findUserByClerkId(userClerkId); + if (!assignedUser) { + return { error: "User not found. Please try again." }; + } + // Check if user is already assigned to a team in this season const teams = await findTeamsBySeasonId(season.id); const userAlreadyHasTeam = teams.some(team => team.ownerId === userClerkId); - + if (userAlreadyHasTeam) { return { error: "This user is already assigned to a team in this league" }; } + // Check if the target team already has an owner + const targetTeam = teams.find(team => team.id === teamId); + if (targetTeam?.ownerId) { + return { error: "This team already has an owner. Remove the current owner first." }; + } + try { - await assignTeamOwner(teamId, userClerkId); + const teamName = `Team ${assignedUser.username || assignedUser.displayName || "Member"}`; + await claimTeam(teamId, userClerkId, teamName); + return { success: true, message: "Owner assigned successfully" }; } catch (error) { console.error("Error assigning team owner:", error); @@ -1132,6 +1146,9 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone

{team.name}

+ {team.ownerId && ( +

{ownerMap[team.ownerId] ?? "Unknown"}

+ )}
{canEditDraftOrder && (
@@ -1349,24 +1366,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone - {isAdmin ? ( - allUsers.map((user) => { - const alreadyCommissioner = commissioners.some( - (c) => c.userId === user.clerkId - ); - return ( - - {user.username || user.displayName || user.email} - {alreadyCommissioner && " (already commissioner)"} - - ); - }) - ) : ( - leagueMembers.map((member) => { + {leagueMembers.map((member) => { const alreadyCommissioner = commissioners.some( (c) => c.userId === member.clerkId ); @@ -1380,8 +1380,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone {alreadyCommissioner && " (already commissioner)"} ); - }) - )} + })}