diff --git a/cutt/db.py b/cutt/db.py index 074ea49..bf27a0b 100644 --- a/cutt/db.py +++ b/cutt/db.py @@ -69,6 +69,16 @@ class Chemistry(SQLModel, table=True): team: int = Field(default=None, foreign_key="team.id") +class PlayerType(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + time: datetime | None = Field(default_factory=utctime) + user: int = Field(default=None, foreign_key="player.id") + handlers: list[int] = Field(sa_column=Column(ARRAY(Integer))) + combis: list[int] = Field(sa_column=Column(ARRAY(Integer))) + cutters: list[int] = Field(sa_column=Column(ARRAY(Integer))) + team: int = Field(default=None, foreign_key="team.id") + + class MVPRanking(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) time: datetime | None = Field(default_factory=utctime) diff --git a/cutt/main.py b/cutt/main.py index 46feb75..238d995 100644 --- a/cutt/main.py +++ b/cutt/main.py @@ -2,7 +2,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles -from cutt.db import Player, Team, Chemistry, MVPRanking, engine +from cutt.db import Player, PlayerType, Team, Chemistry, MVPRanking, engine from sqlmodel import ( Session, func, @@ -20,6 +20,7 @@ from cutt.security import ( from cutt.player import player_router C = Chemistry +PT = PlayerType R = MVPRanking P = Player @@ -167,6 +168,56 @@ def get_chemistry( ) +@api_router.put("/playertype", tags=["analysis"]) +def submit_playertype( + playertype: PlayerType, user: Annotated[Player, Depends(get_current_active_user)] +): + if playertype.team == 42: + return JSONResponse("DEMO team, nothing happens") + if user.id == playertype.user: + with Session(engine) as session: + statement = select(Team).where(Team.id == playertype.team) + players = [t.players for t in session.exec(statement)][0] + if players: + player_ids = {p.id for p in players} + if player_ids >= ( + set(playertype.handlers) + | set(playertype.combis) + | set(playertype.cutters) + ): + session.add(playertype) + session.commit() + return JSONResponse("success!") + raise somethings_fishy + else: + raise wrong_user_id_exception + + +@api_router.get("/playertype/{team_id}", tags=["analysis"]) +def get_playertype( + team_id: int, user: Annotated[Player, Depends(get_current_active_user)] +): + with Session(engine) as session: + subquery = ( + select(PT.user, func.max(PT.time).label("latest")) + .where(PT.user == user.id) + .where(PT.team == team_id) + .group_by(PT.user) + .subquery() + ) + statement2 = select(PT).join( + subquery, (PT.user == subquery.c.user) & (PT.time == subquery.c.latest) + ) + playertype = session.exec(statement2).one_or_none() + if playertype is not None: + return playertype + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="no previous state was found", + ) + + class SPAStaticFiles(StaticFiles): async def get_response(self, path: str, scope): response = await super().get_response(path, scope) diff --git a/src/App.css b/src/App.css index a0ee067..506a45b 100644 --- a/src/App.css +++ b/src/App.css @@ -188,7 +188,6 @@ h3 { display: flex; flex-direction: column; min-height: 32px; - height: 92%; } .box { @@ -198,6 +197,9 @@ h3 { border-style: solid; border-radius: 16px; + h4 { + margin: 4px; + } &.one { max-width: min(96%, 768px); margin: 4px auto; @@ -208,6 +210,7 @@ h3 { } .reservoir { + display: flex; flex-direction: unset; flex-wrap: wrap; justify-content: space-around; diff --git a/src/Rankings.tsx b/src/Rankings.tsx index 32f0fdf..3b670f8 100644 --- a/src/Rankings.tsx +++ b/src/Rankings.tsx @@ -2,7 +2,7 @@ import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react"; import { ReactSortable, ReactSortableProps } from "react-sortablejs"; import { apiAuth, loadPlayers, User } from "./api"; import { TeamState, useSession } from "./Session"; -import { Chemistry, MVPRanking } from "./types"; +import { Chemistry, MVPRanking, PlayerType } from "./types"; import TabController from "./TabController"; type PlayerListProps = Partial> & { @@ -11,7 +11,12 @@ type PlayerListProps = Partial> & { function PlayerList(props: PlayerListProps) { return ( - + {props.list?.map((item, index) => (
{props.orderedList @@ -178,6 +183,144 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) { ); } +function TypeDnD({ user, teams, players }: PlayerInfoProps) { + const [availablePlayers, setAvailablePlayers] = useState(players); + const [handlers, setHandlers] = useState([]); + const [combis, setCombis] = useState([]); + const [cutters, setCutters] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + handleGet(); + }, [players]); + + const [dialog, setDialog] = useState("dialog"); + const dialogRef = useRef(null); + + async function handleSubmit() { + if (dialogRef.current) dialogRef.current.showModal(); + setDialog("sending..."); + let handlerlist = handlers.map(({ id }) => id); + let combilist = combis.map(({ id }) => id); + let cutterlist = cutters.map(({ id }) => id); + const data = { + user: user.id, + handlers: handlerlist, + combis: combilist, + cutters: cutterlist, + team: teams.activeTeam, + }; + const response = await apiAuth("playertype", data, "PUT"); + setDialog(response || "try sending again"); + } + + async function handleGet() { + setLoading(true); + const data = await apiAuth(`playertype/${teams.activeTeam}`, null, "GET"); + if (data.detail) { + console.log(data.detail); + setAvailablePlayers(players); + setHandlers([]); + setCombis([]); + setCutters([]); + } else { + const playertype = data as PlayerType; + setAvailablePlayers( + players.filter( + (player) => + !playertype.handlers.includes(player.id) && + !playertype.combis.includes(player.id) && + !playertype.cutters.includes(player.id) + ) + ); + setHandlers(filterSort(players, playertype.handlers)); + setCombis(filterSort(players, playertype.combis)); + setCutters(filterSort(players, playertype.cutters)); + } + setLoading(false); + } + + return ( + <> + { + setAvailablePlayers(players); + setHandlers([]); + setCombis([]); + setCutters([]); + }} + /> +
+
+ +
+
+
+
+

handler

+ {handlers.length < 1 && ( + + drag people here that you like to see as handlers + + )} + +
+
+

combi

+ {combis.length < 1 && ( + + drag people here that switch between handling and cutting + + )} + +
+
+

cutter

+ {cutters.length < 1 && ( + + drag people here that you think are the best cutters + + )} + +
+
+ + { + event.currentTarget.close(); + }} + > + {dialog} + + + ); +} + function MVPDnD({ user, teams, players }: PlayerInfoProps) { const [availablePlayers, setAvailablePlayers] = useState(players); const [rankedPlayers, setRankedPlayers] = useState([]); @@ -319,6 +462,7 @@ export default function Rankings() { const tabs = [ { id: "Chemistry", label: "๐Ÿงช Chemistry" }, + { id: "Type", label: "๐Ÿƒ Type" }, { id: "MVP", label: "๐Ÿ† MVP" }, ]; @@ -327,6 +471,7 @@ export default function Rankings() { {user && teams && players ? ( + ) : ( diff --git a/src/types.ts b/src/types.ts index cd46efa..661a459 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,6 +27,14 @@ export interface Chemistry { love: number[]; } +export interface PlayerType { + id: number; + user: number; + handlers: number[]; + combis: number[]; + cutters: number[]; +} + export interface MVPRanking { id: number; user: number;