Compare commits

..

No commits in common. "de8dc6b9b97d4890e3012ee0771f00ed8a44737a" and "a6dfab47d5ac44c9c5e410ac9bc097e8a5a4a21a" have entirely different histories.

7 changed files with 29 additions and 336 deletions

View File

@ -8,15 +8,7 @@ from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from sqlmodel import Session, func, select
from sqlmodel.sql.expression import SelectOfScalar
from cutt.db import (
Chemistry,
MVPRanking,
Player,
PlayerTeamLink,
PlayerType,
Team,
engine,
)
from cutt.db import Chemistry, MVPRanking, Player, PlayerTeamLink, Team, engine
import networkx as nx
import numpy as np
import matplotlib
@ -33,7 +25,6 @@ analysis_router = APIRouter(prefix="/analysis", tags=["analysis"])
C = Chemistry
R = MVPRanking
PT = PlayerType
P = Player
@ -306,39 +297,6 @@ async def render_sociogram(params: Params):
return {"image": encoded_image}
translate_tablename = {
R.__tablename__: "🏆",
C.__tablename__: "🧪",
PT.__tablename__: "🃏",
}
def last_submissions(
request: Annotated[TeamScopedRequest, Security(verify_team_scope)],
):
times = {}
with Session(engine) as session:
for survey in [C, PT, R]:
subquery = (
select(survey.user, func.max(survey.time).label("latest"))
.where(survey.team == request.team_id)
.group_by(survey.user)
.subquery()
)
statement2 = select(survey).join(
subquery,
(survey.user == subquery.c.user) & (survey.time == subquery.c.latest),
)
for r in session.exec(statement2):
if r.time.date() not in times:
times[r.time.date()] = {}
times[r.time.date()][r.user] = (
times[r.time.date()].get(r.user, "")
+ translate_tablename[survey.__tablename__]
)
return times
def mvp(
request: Annotated[TeamScopedRequest, Security(verify_team_scope)],
):
@ -472,9 +430,6 @@ analysis_router.add_api_route(
description="Request Most Valuable Players stats",
)
analysis_router.add_api_route("/turnout/{team_id}", endpoint=turnout, methods=["GET"])
analysis_router.add_api_route(
"/times/{team_id}", endpoint=last_submissions, methods=["GET"]
)
if __name__ == "__main__":
with Session(engine) as session:

View File

@ -499,7 +499,7 @@ button {
color: black;
background-color: #36c4;
border: 1px solid black;
border-radius: 1.5em;
border-radius: 1.4em;
margin: 4px;
padding: 0.2em 0.5em;
@ -644,85 +644,3 @@ button {
transform: translateX(0%);
}
}
.calendar-container {
position: relative;
margin: 20px auto;
}
.month-navigation {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
border-top: 2px solid grey;
border-bottom: 2px solid grey;
}
.month-navigation button {
cursor: pointer;
padding: 4px 8px;
border: none;
color: black;
background-color: transparent;
}
.month-navigation span {
font-weight: bold;
}
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.day {
padding: 8px;
border: 1px solid grey;
cursor: pointer;
}
.selected-day {
border: 4px solid grey;
}
.weekday {
border-bottom: 3px solid black;
margin: 0 1em;
}
.day-circle {
text-align: center;
border-radius: 1.5em;
width: 1.5em;
height: 1.5em;
padding: 0;
margin: auto;
border: 2px solid transparent;
}
.today {
border-radius: 1.6em;
border: 4px solid red;
text-align: center;
}
.has-event {
border-radius: 1.5em;
background-color: lightskyblue;
}
.active-player {
border-radius: 1.5em;
border: 4px solid rebeccapurple;
}
.events {
padding: 20px;
ul > li {
padding: 0;
margin: 0;
list-style-type: none;
}
}

View File

@ -1,173 +0,0 @@
import { useEffect, useState } from "react";
import { apiAuth } from "./api";
import { useSession } from "./Session";
interface Datum {
[id: number]: string;
}
interface Events {
[key: string]: Datum;
}
const Calendar = ({ playerId }: { playerId: number }) => {
const [selectedDate, setSelectedDate] = useState(new Date());
const [events, setEvents] = useState<Events>();
const { teams, players } = useSession();
async function loadSubmissionDates() {
if (teams?.activeTeam) {
const data = await apiAuth(`analysis/times/${teams?.activeTeam}`, null);
if (data.detail) {
console.log(data.detail);
} else {
setEvents(data as Events);
}
}
}
useEffect(() => {
loadSubmissionDates();
}, [players]);
const getEventsForDay = (date: Date) => {
return events && events[date.toISOString().split("T")[0]];
};
// Handle day click
const handleDayClick = (date: Date) => {
setSelectedDate(date);
};
// Navigate to previous month
const handlePrevMonth = () => {
const date = new Date(selectedDate);
date.setMonth(date.getMonth() - 1);
setSelectedDate(date);
};
// Navigate to next month
const handleNextMonth = () => {
const date = new Date(selectedDate);
date.setMonth(date.getMonth() + 1);
setSelectedDate(date);
};
// Render month navigation
const renderMonthNavigation = () => {
return (
<div className="month-navigation">
<button onClick={handlePrevMonth}>&lt;</button>
<span>
<button onClick={() => setSelectedDate(new Date())}>📅</button>
{selectedDate.toLocaleString("default", {
month: "long",
year: "numeric",
})}
</span>
<button onClick={handleNextMonth}>&gt;</button>
</div>
);
};
// Render the calendar
const renderCalendar = () => {
const firstDayOfMonth = new Date(
selectedDate.getFullYear(),
selectedDate.getMonth(),
0
).getDay();
const lastDateOfMonth = new Date(
selectedDate.getFullYear(),
selectedDate.getMonth() + 1,
0
).getDate();
let days: JSX.Element[] = [];
let day = 1;
for (let i = 0; i < 7; i++) {
const date = new Date(0);
date.setDate(i + 5);
days.push(
<div key={"weekday_" + i} className="weekday">
{date.toLocaleString("default", {
weekday: "short",
})}
</div>
);
}
// Add empty cells for the first week
for (let i = 0; i < firstDayOfMonth; i++) {
days.push(<div key={"prev" + i} className="empty"></div>);
}
// Render each day of the month
while (day <= lastDateOfMonth) {
const date = new Date(selectedDate);
date.setDate(day);
const todaysEvents = getEventsForDay(date);
days.push(
<div
key={date.getDate()}
className={
"day" +
(date.toDateString() === selectedDate.toDateString()
? " selected-day"
: "")
}
onClick={() => handleDayClick(date)}
>
<div
className={
"day-circle" +
(date.toDateString() === new Date().toDateString()
? " today"
: "") +
(todaysEvents ? " has-event" : "") +
(todaysEvents && playerId in todaysEvents ? " active-player" : "")
}
>
{day}
</div>
</div>
);
day++;
}
return <div className="calendar">{days}</div>;
};
// Render events for the selected day
const renderEvents = () => {
const eventsForDay = getEventsForDay(selectedDate);
return (
<div className="events">
{eventsForDay && (
<ul>
{Object.entries(eventsForDay).map(([id, sub]) => {
const name = players?.filter((p) => p.id === Number(id));
return (
<li key={id}>
{name ? name[0].display_name : ""}:{" "}
<span style={{ letterSpacing: 8 }}>{sub}</span>
</li>
);
})}
</ul>
)}
</div>
);
};
return (
<div className="calendar-container">
{renderMonthNavigation()}
{renderCalendar()}
{renderEvents()}
</div>
);
};
export default Calendar;

View File

@ -15,7 +15,7 @@ export default function Footer() {
</Link>
<span>|</span>
<Link to="/network">
<span>Sociogram</span>
<span>Trainer Analysis</span>
</Link>
<span>|</span>
<Link to="/mvp">

View File

@ -1,6 +1,6 @@
import { ButtonHTMLAttributes, useEffect, useRef, useState } from "react";
import { ReactSortable, ReactSortableProps } from "react-sortablejs";
import { apiAuth, User } from "./api";
import { apiAuth, loadPlayers, User } from "./api";
import { TeamState, useSession } from "./Session";
import { Chemistry, MVPRanking, PlayerType } from "./types";
import TabController from "./TabController";
@ -451,7 +451,14 @@ function HeaderControl({ onLoad, onClear }: HeaderControlProps) {
}
export default function Rankings() {
const { user, teams, players } = useSession();
const { user, teams } = useSession();
const [players, setPlayers] = useState<User[] | null>(null);
useEffect(() => {
if (teams) {
loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
}
}, [user, teams]);
const tabs = [
{ id: "Chemistry", label: "🧪 Chemistry" },

View File

@ -5,7 +5,7 @@ import {
useEffect,
useState,
} from "react";
import { apiAuth, currentUser, loadPlayers, logout, User } from "./api";
import { apiAuth, currentUser, logout, User } from "./api";
import { Login } from "./Login";
import Header from "./Header";
import { Team } from "./types";
@ -23,8 +23,6 @@ export interface Session {
user: User | null;
teams: TeamState | null;
setTeams: (teams: TeamState) => void;
players: User[] | null;
reloadPlayers: () => void;
onLogout: () => void;
}
@ -32,8 +30,6 @@ const sessionContext = createContext<Session>({
user: null,
teams: null,
setTeams: () => {},
players: null,
reloadPlayers: () => {},
onLogout: () => {},
});
@ -42,7 +38,6 @@ export function SessionProvider(props: SessionProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [teams, setTeams] = useState<TeamState | null>(null);
const [players, setPlayers] = useState<User[] | null>(null);
const [err, setErr] = useState<unknown>(null);
const [loading, setLoading] = useState(false);
@ -65,19 +60,12 @@ export function SessionProvider(props: SessionProviderProps) {
if (teams) setTeams({ teams: teams, activeTeam: teams[0].id });
}
async function reloadPlayers() {
teams && loadPlayers(teams?.activeTeam).then((data) => setPlayers(data));
}
useEffect(() => {
loadUser();
}, []);
useEffect(() => {
loadTeam();
}, [user]);
useEffect(() => {
reloadPlayers();
}, [teams]);
function onLogin(user: User) {
setUser(user);
@ -108,9 +96,7 @@ export function SessionProvider(props: SessionProviderProps) {
content = <Login onLogin={onLogin} />;
} else
content = (
<sessionContext.Provider
value={{ user, teams, setTeams, players, reloadPlayers, onLogout }}
>
<sessionContext.Provider value={{ user, teams, setTeams, onLogout }}>
{children}
</sessionContext.Provider>
);

View File

@ -1,12 +1,11 @@
import { FormEvent, useEffect, useState } from "react";
import { apiAuth, Gender, User } from "./api";
import { apiAuth, Gender, loadPlayers, User } from "./api";
import { useSession } from "./Session";
import { ErrorState } from "./types";
import { useNavigate } from "react-router";
import Calendar from "./Calendar";
const TeamPanel = () => {
const { user, teams, players, reloadPlayers } = useSession();
const { user, teams } = useSession();
const navigate = useNavigate();
useEffect(() => {
user?.scopes.includes(`team:${teams?.activeTeam}`) ||
@ -22,8 +21,16 @@ const TeamPanel = () => {
email: "",
} as User;
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]);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (teams) {
@ -32,14 +39,14 @@ const TeamPanel = () => {
if (r.detail) setError({ ok: false, message: r.detail });
else {
setError({ ok: true, message: r });
reloadPlayers();
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 });
reloadPlayers();
loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
}
}
}
@ -59,7 +66,7 @@ const TeamPanel = () => {
else {
setError({ ok: true, message: r });
setPlayer(newPlayerTemplate);
reloadPlayers();
loadPlayers(teams.activeTeam).then((data) => setPlayers(data));
}
}
}
@ -90,11 +97,7 @@ const TeamPanel = () => {
{players &&
players.map((p) => (
<button
className={
"team-player " +
p.gender +
(p.id === player.id ? " active-player" : "")
}
className={"team-player " + p.gender}
key={p.id}
onClick={() => {
setPlayer(p);
@ -130,10 +133,8 @@ const TeamPanel = () => {
onChange={(e) => {
setPlayer({
...player,
...(player.id === 0 && {
username: e.target.value.toLowerCase().replace(/\W/g, ""),
}),
display_name: e.target.value,
username: e.target.value.toLowerCase().replace(/\W/g, ""),
});
setError({ ok: true, message: "" });
}}
@ -218,7 +219,6 @@ const TeamPanel = () => {
)}
</form>
</div>
<Calendar playerId={player.id} />
</div>
);
} else <span className="loader" />;