feat: add player type survey
This commit is contained in:
parent
a6ebc28d47
commit
b9efd4f7a3
10
cutt/db.py
10
cutt/db.py
@ -69,6 +69,16 @@ class Chemistry(SQLModel, table=True):
|
|||||||
team: int = Field(default=None, foreign_key="team.id")
|
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):
|
class MVPRanking(SQLModel, table=True):
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
id: int | None = Field(default=None, primary_key=True)
|
||||||
time: datetime | None = Field(default_factory=utctime)
|
time: datetime | None = Field(default_factory=utctime)
|
||||||
|
53
cutt/main.py
53
cutt/main.py
@ -2,7 +2,7 @@ from typing import Annotated
|
|||||||
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Security, status
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
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 (
|
from sqlmodel import (
|
||||||
Session,
|
Session,
|
||||||
func,
|
func,
|
||||||
@ -20,6 +20,7 @@ from cutt.security import (
|
|||||||
from cutt.player import player_router
|
from cutt.player import player_router
|
||||||
|
|
||||||
C = Chemistry
|
C = Chemistry
|
||||||
|
PT = PlayerType
|
||||||
R = MVPRanking
|
R = MVPRanking
|
||||||
P = Player
|
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):
|
class SPAStaticFiles(StaticFiles):
|
||||||
async def get_response(self, path: str, scope):
|
async def get_response(self, path: str, scope):
|
||||||
response = await super().get_response(path, scope)
|
response = await super().get_response(path, scope)
|
||||||
|
@ -188,7 +188,6 @@ h3 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
height: 92%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
@ -198,6 +197,9 @@ h3 {
|
|||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
&.one {
|
&.one {
|
||||||
max-width: min(96%, 768px);
|
max-width: min(96%, 768px);
|
||||||
margin: 4px auto;
|
margin: 4px auto;
|
||||||
@ -208,6 +210,7 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.reservoir {
|
.reservoir {
|
||||||
|
display: flex;
|
||||||
flex-direction: unset;
|
flex-direction: unset;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
149
src/Rankings.tsx
149
src/Rankings.tsx
@ -2,7 +2,7 @@ import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react";
|
|||||||
import { ReactSortable, ReactSortableProps } from "react-sortablejs";
|
import { ReactSortable, ReactSortableProps } from "react-sortablejs";
|
||||||
import { apiAuth, loadPlayers, User } from "./api";
|
import { apiAuth, loadPlayers, User } from "./api";
|
||||||
import { TeamState, useSession } from "./Session";
|
import { TeamState, useSession } from "./Session";
|
||||||
import { Chemistry, MVPRanking } from "./types";
|
import { Chemistry, MVPRanking, PlayerType } from "./types";
|
||||||
import TabController from "./TabController";
|
import TabController from "./TabController";
|
||||||
|
|
||||||
type PlayerListProps = Partial<ReactSortableProps<any>> & {
|
type PlayerListProps = Partial<ReactSortableProps<any>> & {
|
||||||
@ -11,7 +11,12 @@ type PlayerListProps = Partial<ReactSortableProps<any>> & {
|
|||||||
|
|
||||||
function PlayerList(props: PlayerListProps) {
|
function PlayerList(props: PlayerListProps) {
|
||||||
return (
|
return (
|
||||||
<ReactSortable {...props} animation={200} swapThreshold={0.4}>
|
<ReactSortable
|
||||||
|
{...props}
|
||||||
|
animation={200}
|
||||||
|
swapThreshold={0.2}
|
||||||
|
style={{ minHeight: props.list?.length < 1 ? 64 : 32 }}
|
||||||
|
>
|
||||||
{props.list?.map((item, index) => (
|
{props.list?.map((item, index) => (
|
||||||
<div key={item.id} className="item">
|
<div key={item.id} className="item">
|
||||||
{props.orderedList
|
{props.orderedList
|
||||||
@ -178,6 +183,144 @@ function ChemistryDnD({ user, teams, players }: PlayerInfoProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TypeDnD({ user, teams, players }: PlayerInfoProps) {
|
||||||
|
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
|
||||||
|
const [handlers, setHandlers] = useState<User[]>([]);
|
||||||
|
const [combis, setCombis] = useState<User[]>([]);
|
||||||
|
const [cutters, setCutters] = useState<User[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleGet();
|
||||||
|
}, [players]);
|
||||||
|
|
||||||
|
const [dialog, setDialog] = useState("dialog");
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(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 (
|
||||||
|
<>
|
||||||
|
<HeaderControl
|
||||||
|
onLoad={handleGet}
|
||||||
|
onClear={() => {
|
||||||
|
setAvailablePlayers(players);
|
||||||
|
setHandlers([]);
|
||||||
|
setCombis([]);
|
||||||
|
setCutters([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="container">
|
||||||
|
<div className="box one">
|
||||||
|
<PlayerList
|
||||||
|
list={availablePlayers}
|
||||||
|
setList={setAvailablePlayers}
|
||||||
|
group={"type-shared"}
|
||||||
|
className="dragbox reservoir"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="container">
|
||||||
|
<div className="box three">
|
||||||
|
<h4>handler</h4>
|
||||||
|
{handlers.length < 1 && (
|
||||||
|
<span className="grey hint">
|
||||||
|
drag people here that you like to see as handlers
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<PlayerList
|
||||||
|
list={handlers}
|
||||||
|
setList={setHandlers}
|
||||||
|
group={"type-shared"}
|
||||||
|
className="dragbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="box three">
|
||||||
|
<h4>combi</h4>
|
||||||
|
{combis.length < 1 && (
|
||||||
|
<span className="grey hint">
|
||||||
|
drag people here that switch between handling and cutting
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<PlayerList
|
||||||
|
list={combis}
|
||||||
|
setList={setCombis}
|
||||||
|
group={"type-shared"}
|
||||||
|
className="middle dragbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="box three">
|
||||||
|
<h4>cutter</h4>
|
||||||
|
{cutters.length < 1 && (
|
||||||
|
<span className="grey hint">
|
||||||
|
drag people here that you think are the best cutters
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<PlayerList
|
||||||
|
list={cutters}
|
||||||
|
setList={setCutters}
|
||||||
|
group={"type-shared"}
|
||||||
|
className="dragbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="submit wavering" onClick={() => handleSubmit()}>
|
||||||
|
💾 <span className="submit_text">submit</span>
|
||||||
|
</button>
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
id="PlayerTypeDialog"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.currentTarget.close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dialog}
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MVPDnD({ user, teams, players }: PlayerInfoProps) {
|
function MVPDnD({ user, teams, players }: PlayerInfoProps) {
|
||||||
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
|
const [availablePlayers, setAvailablePlayers] = useState<User[]>(players);
|
||||||
const [rankedPlayers, setRankedPlayers] = useState<User[]>([]);
|
const [rankedPlayers, setRankedPlayers] = useState<User[]>([]);
|
||||||
@ -319,6 +462,7 @@ export default function Rankings() {
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: "Chemistry", label: "🧪 Chemistry" },
|
{ id: "Chemistry", label: "🧪 Chemistry" },
|
||||||
|
{ id: "Type", label: "🃏 Type" },
|
||||||
{ id: "MVP", label: "🏆 MVP" },
|
{ id: "MVP", label: "🏆 MVP" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -327,6 +471,7 @@ export default function Rankings() {
|
|||||||
{user && teams && players ? (
|
{user && teams && players ? (
|
||||||
<TabController tabs={tabs}>
|
<TabController tabs={tabs}>
|
||||||
<ChemistryDnD {...{ user, teams, players }} />
|
<ChemistryDnD {...{ user, teams, players }} />
|
||||||
|
<TypeDnD {...{ user, teams, players }} />
|
||||||
<MVPDnD {...{ user, teams, players }} />
|
<MVPDnD {...{ user, teams, players }} />
|
||||||
</TabController>
|
</TabController>
|
||||||
) : (
|
) : (
|
||||||
|
@ -27,6 +27,14 @@ export interface Chemistry {
|
|||||||
love: number[];
|
love: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlayerType {
|
||||||
|
id: number;
|
||||||
|
user: number;
|
||||||
|
handlers: number[];
|
||||||
|
combis: number[];
|
||||||
|
cutters: number[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface MVPRanking {
|
export interface MVPRanking {
|
||||||
id: number;
|
id: number;
|
||||||
user: number;
|
user: number;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user