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:
parent
41384f08fb
commit
9d1765d8a4
3 changed files with 42 additions and 26 deletions
|
|
@ -83,6 +83,14 @@ export async function assignTeamOwner(
|
||||||
return await updateTeam(id, { ownerId });
|
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> {
|
export async function removeTeamOwner(id: string): Promise<Team> {
|
||||||
return await updateTeam(id, { ownerId: null });
|
return await updateTeam(id, { ownerId: null });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import {
|
||||||
findLeagueById,
|
findLeagueById,
|
||||||
findCommissionersByLeagueId,
|
findCommissionersByLeagueId,
|
||||||
findAvailableTeams,
|
findAvailableTeams,
|
||||||
assignTeamOwner,
|
claimTeam,
|
||||||
isUserLeagueMember,
|
isUserLeagueMember,
|
||||||
|
findUserByClerkId,
|
||||||
} from "~/models";
|
} from "~/models";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -94,8 +95,16 @@ export async function action(args: Route.ActionArgs) {
|
||||||
throw new Response("No available teams in this league", { status: 400 });
|
throw new Response("No available teams in this league", { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign the first available team to the user
|
// Look up user before claiming to fail fast and get their name
|
||||||
await assignTeamOwner(availableTeams[0].id, userId);
|
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
|
// Redirect to league page
|
||||||
return redirect(`/leagues/${season.leagueId}?joined=true`);
|
return redirect(`/leagues/${season.leagueId}?joined=true`);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
removeCommissionerByLeagueAndUser,
|
removeCommissionerByLeagueAndUser,
|
||||||
} from "~/models/commissioner";
|
} from "~/models/commissioner";
|
||||||
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
|
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 { findAllUsers, findUserByClerkId, isUserAdminByClerkId } from "~/models/user";
|
||||||
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
|
||||||
import { findAllSportsSeasons } from "~/models/sports-season";
|
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" };
|
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
|
// Check if user is already assigned to a team in this season
|
||||||
const teams = await findTeamsBySeasonId(season.id);
|
const teams = await findTeamsBySeasonId(season.id);
|
||||||
const userAlreadyHasTeam = teams.some(team => team.ownerId === userClerkId);
|
const userAlreadyHasTeam = teams.some(team => team.ownerId === userClerkId);
|
||||||
|
|
||||||
if (userAlreadyHasTeam) {
|
if (userAlreadyHasTeam) {
|
||||||
return { error: "This user is already assigned to a team in this league" };
|
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 {
|
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" };
|
return { success: true, message: "Owner assigned successfully" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error assigning team owner:", error);
|
console.error("Error assigning team owner:", error);
|
||||||
|
|
@ -1132,6 +1146,9 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-medium">{team.name}</p>
|
<p className="font-medium">{team.name}</p>
|
||||||
|
{team.ownerId && (
|
||||||
|
<p className="text-xs text-muted-foreground">{ownerMap[team.ownerId] ?? "Unknown"}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{canEditDraftOrder && (
|
{canEditDraftOrder && (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
|
@ -1349,24 +1366,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
<SelectValue placeholder="Select a user" />
|
<SelectValue placeholder="Select a user" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{isAdmin ? (
|
{leagueMembers.map((member) => {
|
||||||
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) => {
|
|
||||||
const alreadyCommissioner = commissioners.some(
|
const alreadyCommissioner = commissioners.some(
|
||||||
(c) => c.userId === member.clerkId
|
(c) => c.userId === member.clerkId
|
||||||
);
|
);
|
||||||
|
|
@ -1380,8 +1380,7 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
||||||
{alreadyCommissioner && " (already commissioner)"}
|
{alreadyCommissioner && " (already commissioner)"}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
);
|
);
|
||||||
})
|
})}
|
||||||
)}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue