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 <username>" (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 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-18 00:46:15 -07:00
parent 41384f08fb
commit 7f26b74b2b
3 changed files with 40 additions and 6 deletions

View file

@ -83,6 +83,14 @@ export async function assignTeamOwner(
return await updateTeam(id, { ownerId });
}
export async function claimTeam(
id: string,
ownerId: string,
name: string
): Promise<Team> {
return await updateTeam(id, { ownerId, name });
}
export async function removeTeamOwner(id: string): Promise<Team> {
return await updateTeam(id, { ownerId: null });
}

View file

@ -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`);

View file

@ -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
</div>
<div className="flex-1">
<p className="font-medium">{team.name}</p>
{team.ownerId && (
<p className="text-xs text-muted-foreground">{ownerMap[team.ownerId] ?? "Unknown"}</p>
)}
</div>
{canEditDraftOrder && (
<div className="flex gap-1">