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 <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>

* Fixing dropdown for admins

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-18 00:53:40 -07:00 committed by GitHub
parent 41384f08fb
commit 9d1765d8a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 42 additions and 26 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,6 +361,12 @@ 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);
@ -369,8 +375,16 @@ export async function action(args: Route.ActionArgs) {
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">
@ -1349,24 +1366,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
<SelectValue placeholder="Select a user" />
</SelectTrigger>
<SelectContent>
{isAdmin ? (
allUsers.map((user) => {
const alreadyCommissioner = commissioners.some(
(c) => c.userId === user.clerkId
);
return (
<SelectItem
key={user.id}
value={user.clerkId}
disabled={alreadyCommissioner}
>
{user.username || user.displayName || user.email}
{alreadyCommissioner && " (already commissioner)"}
</SelectItem>
);
})
) : (
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)"}
</SelectItem>
);
})
)}
})}
</SelectContent>
</Select>
<Button