appoint team manager in UI

This commit is contained in:
2026-01-03 13:59:53 +01:00
parent 12b42e4a46
commit 051202edc9
4 changed files with 118 additions and 26 deletions

View File

@@ -19,12 +19,24 @@ P = Player
player_router = APIRouter(prefix="/player", tags=["player"]) player_router = APIRouter(prefix="/player", tags=["player"])
def update_team_manager(scopes_str: str, team_id: int, state: bool = True):
scopes = set(scopes_str.split())
if state:
scopes.add(f"team:{team_id}")
else:
scopes.remove(f"team:{team_id}")
print("new scopestr", " ".join(scopes))
return " ".join(sorted(scopes))
class PlayerRequest(BaseModel): class PlayerRequest(BaseModel):
display_name: str display_name: str
username: str username: str
gender: str | None gender: str | None
number: str number: str
email: str | None email: str | None
is_manager: bool | None
class AddPlayerRequest(PlayerRequest): ... class AddPlayerRequest(PlayerRequest): ...
@@ -96,6 +108,10 @@ def modify_player(
player.number = r.number.strip() player.number = r.number.strip()
player.gender = r.gender.strip() if r.gender else None player.gender = r.gender.strip() if r.gender else None
player.email = r.email.strip() if r.email else None player.email = r.email.strip() if r.email else None
if r.is_manager is not None:
player.scopes = update_team_manager(
player.scopes, request.team_id, r.is_manager
)
session.add(player) session.add(player)
session.commit() session.commit()
return PlainTextResponse("modification successful") return PlainTextResponse("modification successful")
@@ -212,6 +228,8 @@ async def list_players(
] + demo_players ] + demo_players
allowed_scopes = set(user.scopes.split()) allowed_scopes = set(user.scopes.split())
team_manager_scope = f"team:{team_id}"
is_team_manager = team_manager_scope in allowed_scopes
with Session(engine) as session: with Session(engine) as session:
current_user = session.exec( current_user = session.exec(
@@ -220,7 +238,7 @@ async def list_players(
.join(Team) .join(Team)
.where(Team.id == team_id, P.disabled == False, P.id == user.id) .where(Team.id == team_id, P.disabled == False, P.id == user.id)
).one_or_none() ).one_or_none()
if not current_user and f"team:{team_id}" not in allowed_scopes: if not current_user and not is_team_manager:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="you're not in this team", detail="you're not in this team",
@@ -233,21 +251,26 @@ async def list_players(
.where(Team.id == team_id, P.disabled == False) .where(Team.id == team_id, P.disabled == False)
.order_by(P.display_name) .order_by(P.display_name)
).all() ).all()
if players: if players:
return [ players_dump = []
player.model_dump( for player in players:
include={ if not player.disabled:
"id", player_dump = player.model_dump(
"display_name", include={
"username", "id",
"gender", "display_name",
"number", "username",
"email", "gender",
} "number",
) }
for player in players )
if not player.disabled if is_team_manager:
] player_dump["email"] = player.email
if team_manager_scope in player.scopes:
player_dump["is_manager"] = True
players_dump.append(player_dump)
return players_dump
def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]): def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]):

View File

@@ -4,6 +4,7 @@ import { useSession } from "./Session";
import { ErrorState } from "./types"; import { ErrorState } from "./types";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import Calendar from "./Calendar"; import Calendar from "./Calendar";
import { Info, Star, StarHalf, StarOff, UserPen } from "lucide-react";
import Loading from "./Loading"; import Loading from "./Loading";
const TeamPanel = () => { const TeamPanel = () => {
@@ -21,6 +22,7 @@ const TeamPanel = () => {
gender: undefined, gender: undefined,
number: "", number: "",
email: "", email: "",
is_manager: false,
} as User; } as User;
const [error, setError] = useState<ErrorState>(); const [error, setError] = useState<ErrorState>();
const [player, setPlayer] = useState(newPlayerTemplate); const [player, setPlayer] = useState(newPlayerTemplate);
@@ -96,7 +98,12 @@ const TeamPanel = () => {
setError({ ok: true, message: "" }); setError({ ok: true, message: "" });
}} }}
> >
{p.display_name} <span>{p.display_name}</span>
{p.is_manager && (
<span className="icon">
<Star size={16} fill="gold" />
</span>
)}
</button> </button>
))} ))}
<button <button
@@ -197,7 +204,7 @@ const TeamPanel = () => {
</div> </div>
<div className="field"> <div className="field">
<label className="label"> <label className="label">
email <small>(optional)</small> email <small>(optional, but helpful)</small>
</label> </label>
<div className="control"> <div className="control">
<input <input
@@ -210,18 +217,52 @@ const TeamPanel = () => {
}} }}
/> />
</div> </div>
{error?.message && ( </div>
<p <div className="field">
className={ <div className="control">
"help" + (error.ok ? " is-success" : " is-danger") <label className="label">
} manager{" "}
<span className="tooltip">
<Info size={16} />
<span className="tooltiptext notification is-primary is-light has-text-centered">
managers are able to see the analyses and modify
the players
</span>
</span>
</label>
<button
className={"button"}
onClick={(e) => {
e.preventDefault();
setPlayer({
...player,
is_manager: !player.is_manager,
});
setError({ ok: true, message: "" });
}}
> >
{error.message} <span className="icon">
</p> {player.is_manager ? (
)} <Star fill="gold" />
) : (
<StarOff />
)}
</span>
<span>{player.is_manager ? "yes" : "no"}</span>
</button>
</div>
</div> </div>
</div> </div>
<div className="field is-grouped is-grouped-centered"> {error?.message && (
<p
className={
"help" + (error.ok ? " is-success" : " is-danger")
}
>
{error.message}
</p>
)}
<div className="field is-grouped is-grouped-multiline is-grouped-centered">
<button <button
className={ className={
"button is-light" + "button is-light" +

View File

@@ -1,3 +1,4 @@
import { JwtPayload } from "jwt-decode";
import { useSession } from "./Session"; import { useSession } from "./Session";
export const baseUrl = ""; export const baseUrl = "";
@@ -53,6 +54,7 @@ export type User = {
number: string; number: string;
gender: Gender; gender: Gender;
scopes: string; scopes: string;
is_manager: boolean;
}; };
export async function currentUser(): Promise<User> { export async function currentUser(): Promise<User> {
@@ -124,3 +126,9 @@ export const logout = async () => {
console.error(e); console.error(e);
} }
}; };
export interface PassToken extends JwtPayload {
username: string;
name: string;
team_id: number;
}

View File

@@ -20,3 +20,23 @@
position: absolute; position: absolute;
margin: 1rem; margin: 1rem;
} }
.tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
/* Tooltip text */
.tooltiptext {
visibility: hidden;
width: 10rem;
padding: 0.25rem;
position: absolute;
z-index: 1;
}
/* Show the tooltip text on hover */
.tooltip:hover .tooltiptext {
visibility: visible;
}