From 8b4ee3b28997a66a324598b913263b7752590a22 Mon Sep 17 00:00:00 2001
From: julius <julius@0124816.xyz>
Date: Mon, 24 Mar 2025 14:11:58 +0100
Subject: [PATCH] feat: Team management panel

the display name of a player is the same for all teams... change that?
---
 cutt/player.py    |  90 +++++++++++++++++++++++++++++++---
 src/App.css       |   6 +++
 src/TeamPanel.tsx | 122 ++++++++++++++++++++++++++++++++++++----------
 src/api.ts        |   2 +-
 src/types.ts      |   5 ++
 5 files changed, 191 insertions(+), 34 deletions(-)

diff --git a/cutt/player.py b/cutt/player.py
index ce91d8c..86f45bb 100644
--- a/cutt/player.py
+++ b/cutt/player.py
@@ -18,13 +18,16 @@ P = Player
 player_router = APIRouter(prefix="/player", tags=["player"])
 
 
-class AddPlayerRequest(BaseModel):
+class PlayerRequest(BaseModel):
     display_name: str
     username: str
     number: str
     email: str
 
 
+class AddPlayerRequest(PlayerRequest): ...
+
+
 def add_player(
     r: AddPlayerRequest,
     request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
@@ -57,6 +60,63 @@ def add_player(
         )
         session.add(new_player)
         session.commit()
+        return PlainTextResponse(f"added {new_player.display_name}")
+
+
+class ModifyPlayerRequest(PlayerRequest):
+    id: int
+
+
+def modify_player(
+    r: ModifyPlayerRequest,
+    request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
+):
+    with Session(engine) as session:
+        player = session.exec(
+            select(P)
+            .join(PlayerTeamLink)
+            .join(Team)
+            .where(Team.id == request.team_id, P.id == r.id, P.username == r.username)
+        ).one_or_none()
+        if player:
+            player.display_name = r.display_name.strip()
+            player.number = r.number.strip()
+            player.email = r.email.strip()
+            session.add(player)
+            session.commit()
+            return PlainTextResponse("modification successful")
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_404_NOT_FOUND,
+                detail="no such player found in your team",
+            )
+
+
+class DisablePlayerRequest(BaseModel):
+    player_id: int
+
+
+def disable_player(
+    r: DisablePlayerRequest,
+    request: Annotated[TeamScopedRequest, Depends(verify_team_scope)],
+):
+    with Session(engine) as session:
+        player = session.exec(
+            select(P)
+            .join(PlayerTeamLink)
+            .join(Team)
+            .where(Team.id == request.team_id, P.id == r.player_id)
+        ).one_or_none()
+        if player:
+            player.disabled = True
+            session.add(player)
+            session.commit()
+            return PlainTextResponse(f"disabled {player.display_name}")
+        else:
+            raise HTTPException(
+                status_code=status.HTTP_404_NOT_FOUND,
+                detail="no such player found in your team",
+            )
 
 
 def add_player_to_team(player_id: int, team_id: int):
@@ -86,11 +146,17 @@ async def list_all_players():
 
 async def list_players(team_id: int):
     with Session(engine) as session:
-        statement = select(Team).where(Team.id == team_id)
-        players = [t.players for t in session.exec(statement)][0]
+        players = session.exec(
+            select(P)
+            .join(PlayerTeamLink)
+            .join(Team)
+            .where(Team.id == team_id, P.disabled == False)
+        ).all()
         if players:
             return [
-                player.model_dump(include={"id", "display_name", "username", "number"})
+                player.model_dump(
+                    include={"id", "display_name", "username", "number", "email"}
+                )
                 for player in players
                 if not player.disabled
             ]
@@ -102,12 +168,22 @@ def read_teams_me(user: Annotated[P, Depends(get_current_active_user)]):
 
 
 player_router.add_api_route(
-    "/add/{team_id}",
+    "/{team_id}",
     endpoint=add_player,
     methods=["POST"],
 )
 player_router.add_api_route(
-    "/list/{team_id}",
+    "/{team_id}",
+    endpoint=modify_player,
+    methods=["PUT"],
+)
+player_router.add_api_route(
+    "/{team_id}",
+    endpoint=disable_player,
+    methods=["DELETE"],
+)
+player_router.add_api_route(
+    "/{team_id}/list",
     endpoint=list_players,
     methods=["GET"],
     dependencies=[Depends(get_current_active_user)],
@@ -119,7 +195,7 @@ player_router.add_api_route(
     dependencies=[Security(get_current_active_user, scopes=["admin"])],
 )
 player_router.add_api_route(
-    "/add/{player_id}/{team_id}",
+    "/add/{team_id}/{player_id}",
     endpoint=add_player_to_team,
     methods=["GET"],
     dependencies=[Security(get_current_active_user, scopes=["admin"])],
diff --git a/src/App.css b/src/App.css
index f744301..852af31 100644
--- a/src/App.css
+++ b/src/App.css
@@ -507,9 +507,15 @@ button {
   &.new-player {
     background-color: #3838;
   }
+
+  &.disable-player {
+    background-color: #e338;
+  }
 }
 
 .new-player-inputs {
+  display: flex;
+  flex-direction: column;
   margin: auto;
 
   div {
diff --git a/src/TeamPanel.tsx b/src/TeamPanel.tsx
index 8074e79..b4c1b5c 100644
--- a/src/TeamPanel.tsx
+++ b/src/TeamPanel.tsx
@@ -1,6 +1,7 @@
 import { FormEvent, useEffect, useState } from "react";
 import { apiAuth, loadPlayers, User } from "./api";
 import { useSession } from "./Session";
+import { ErrorState } from "./types";
 
 export const TeamPanel = () => {
   const { teams } = useSession();
@@ -11,12 +12,13 @@ export const TeamPanel = () => {
     number: "",
     email: "",
   } as User;
-  const [error, setError] = useState("");
+  const [error, setError] = useState<ErrorState>();
   const [players, setPlayers] = useState<User[] | null>(null);
   const [player, setPlayer] = useState(newPlayerTemplate);
 
   useEffect(() => {
     if (teams) {
+      setError({ ok: true, message: "" });
       loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
     }
   }, [teams]);
@@ -24,16 +26,45 @@ export const TeamPanel = () => {
   async function handleSubmit(e: FormEvent) {
     e.preventDefault();
     if (teams) {
-      const r = await apiAuth(
-        `player/add/${teams?.activeTeam}`,
-        player,
-        "POST"
-      );
-      if (r.detail) setError(r.detail);
+      if (player.id === 0) {
+        const r = await apiAuth(`player/${teams?.activeTeam}`, player, "POST");
+        if (r.detail) setError({ ok: false, message: r.detail });
+        else {
+          setError({ ok: true, message: r });
+          loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
+        }
+      } else {
+        const r = await apiAuth(`player/${teams?.activeTeam}`, player, "PUT");
+        if (r.detail) setError({ ok: false, message: r.detail });
+        else {
+          setError({ ok: true, message: r });
+          loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
+        }
+      }
     }
   }
 
-  if (teams) {
+  async function handleDisable(e: FormEvent) {
+    e.preventDefault();
+    if (teams && player.id !== 0) {
+      var confirmation = confirm("are you sure?");
+      if (confirmation) {
+        const r = await apiAuth(
+          `player/${teams?.activeTeam}`,
+          { player_id: player.id },
+          "DELETE"
+        );
+        if (r.detail) setError({ ok: false, message: r.detail });
+        else {
+          setError({ ok: true, message: r });
+          setPlayer(newPlayerTemplate);
+          loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
+        }
+      }
+    }
+  }
+
+  if (teams && players) {
     const activeTeam = teams.teams.filter(
       (team) => team.id == teams?.activeTeam
     )[0];
@@ -60,7 +91,10 @@ export const TeamPanel = () => {
                   <button
                     className="team-player"
                     key={p.id}
-                    onClick={() => setPlayer(p)}
+                    onClick={() => {
+                      setPlayer(p);
+                      setError({ ok: true, message: "" });
+                    }}
                   >
                     {p.display_name}
                   </button>
@@ -68,7 +102,10 @@ export const TeamPanel = () => {
               <button
                 className="team-player new-player"
                 key="add-player"
-                onClick={() => setPlayer(newPlayerTemplate)}
+                onClick={() => {
+                  setPlayer(newPlayerTemplate);
+                  setError({ ok: true, message: "" });
+                }}
               >
                 +
               </button>
@@ -85,13 +122,14 @@ export const TeamPanel = () => {
                 type="text"
                 required
                 value={player.display_name}
-                onChange={(e) =>
+                onChange={(e) => {
                   setPlayer({
                     ...player,
                     display_name: e.target.value,
                     username: e.target.value.toLowerCase().replace(/\W/g, ""),
-                  })
-                }
+                  });
+                  setError({ ok: true, message: "" });
+                }}
               />
             </div>
             <div>
@@ -99,30 +137,62 @@ export const TeamPanel = () => {
               <input
                 type="text"
                 required
+                disabled={player.id !== 0}
                 value={player.username}
-                onChange={(e) =>
-                  setPlayer({ ...player, username: e.target.value })
-                }
+                onChange={(e) => {
+                  setPlayer({ ...player, username: e.target.value });
+                  setError({ ok: true, message: "" });
+                }}
               />
             </div>
             <div>
-              <label>number</label>
+              <label>number (optional)</label>
               <input
                 type="text"
-                value={player.number}
-                onChange={(e) =>
-                  setPlayer({ ...player, number: e.target.value })
-                }
+                value={player.number || ""}
+                onChange={(e) => {
+                  setPlayer({ ...player, number: e.target.value });
+                  setError({ ok: true, message: "" });
+                }}
               />
             </div>
             <div>
-              {error && (
-                <span style={{ color: "red", margin: "auto" }}>{error}</span>
+              <label>email (optional)</label>
+              <input
+                type="email"
+                value={player.email || ""}
+                onChange={(e) => {
+                  setPlayer({ ...player, email: e.target.value });
+                  setError({ ok: true, message: "" });
+                }}
+              />
+            </div>
+            <div style={{ margin: "auto" }}>
+              {error?.message && (
+                <span
+                  style={{
+                    color: error.ok ? "green" : "red",
+                  }}
+                >
+                  {error.message}
+                </span>
               )}
             </div>
-            <button className="team-player new-player">
-              {player.id === 0 ? "add player" : "modify player"}
-            </button>
+            <div style={{ margin: "auto" }}>
+              <button className="team-player new-player">
+                {player.id === 0 ? "add player" : "modify player"}
+              </button>
+            </div>
+            {player.id !== 0 && (
+              <div style={{ margin: "auto" }}>
+                <button
+                  className="team-player disable-player"
+                  onClick={handleDisable}
+                >
+                  remove player
+                </button>
+              </div>
+            )}
           </form>
         </div>
       </div>
diff --git a/src/api.ts b/src/api.ts
index 6649c9c..c5709a3 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -78,7 +78,7 @@ export async function currentUser(): Promise<User> {
 
 export async function loadPlayers(teamId: number) {
   try {
-    const data = await apiAuth(`player/list/${teamId}`, null, "GET");
+    const data = await apiAuth(`player/${teamId}/list`, null, "GET");
     return data as User[];
   } catch (error) {
     console.error(error);
diff --git a/src/types.ts b/src/types.ts
index e3151e8..cd46efa 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -39,3 +39,8 @@ export interface Team {
   location: string;
   country: string;
 }
+
+export type ErrorState = {
+  ok: boolean;
+  message: string;
+};