diff --git a/app/(authenticated)/sports/[id]/create-league/page.tsx b/app/(authenticated)/sports/[id]/create-league/page.tsx index 806fd76..379a498 100644 --- a/app/(authenticated)/sports/[id]/create-league/page.tsx +++ b/app/(authenticated)/sports/[id]/create-league/page.tsx @@ -1,28 +1,30 @@ import {Breadcrumbs, Link, Stack, Typography} from "@mui/material"; import CardBackground from "@/components/layout/cardBackground"; -import LeagueDnd from "@/components/league/create/leagueDnd"; import {sportFactory} from "@/src/models/SportModel"; +import GameForm from "@/components/league/legacy/GameForm"; +import {tagFactory} from "@/src/models/TagModel"; export default async function LeaguePage({params}: { params: { id: string } }) { const sportId = parseInt(params.id, 10) const sport = await sportFactory().show(sportId) - const sportLink = `/sports/${sportId}` + const tags = await tagFactory().index() + return ( - + 管理者のダッシュボード 競技管理 - + {sport.name} リーグを作成・編集 - - + + ); diff --git a/app/(authenticated)/sports/[id]/games/[gameId]/page.tsx b/app/(authenticated)/sports/[id]/games/[gameId]/page.tsx index 747308b..73d4f43 100644 --- a/app/(authenticated)/sports/[id]/games/[gameId]/page.tsx +++ b/app/(authenticated)/sports/[id]/games/[gameId]/page.tsx @@ -4,17 +4,32 @@ import {sportFactory} from "@/src/models/SportModel"; import {gameFactory} from "@/src/models/GameModel"; import MatchList from "@/components/match/matchList"; import LeagueTable from "@/components/league/table/leagueTable"; +import {tagFactory} from "@/src/models/TagModel"; +import GameForm from "@/components/league/legacy/GameForm"; +import {teamTagFactory} from "@/src/models/TeamTagModel"; +import {teamFactory} from "@/src/models/TeamModel"; +import {GameEntryList} from "@/components/league/legacy/GameEntryList"; +import AddGameEntryDialog from "@/components/league/legacy/AddGameEntryDialog"; -export default async function GamePage({params}: { params: { gameId:string, id: string } }) { +export default async function GamePage({params}: { params: { gameId: string, id: string } }) { const gameId = parseInt(params.gameId, 10) const game = await gameFactory().show(gameId) const sportId = parseInt(params.id, 10) const sport = await sportFactory().show(sportId) const matchList = await gameFactory().getGameMatches(gameId) + const tags = await tagFactory().index() - return( + // この突貫工事は修正してください。 + const gameEntryTeams = await gameFactory().getGameEntries(gameId) + const teamTags = await teamTagFactory().index() + const linkedTeamTag = teamTags.find((teamTag) => teamTag.sportId === sport.id) + const teams = await teamFactory().index() + const filteredTeams = linkedTeamTag ? teams.filter((team) => team.teamTagId === linkedTeamTag.id) : [] + + + return ( - + 管理者のダッシュボード @@ -26,10 +41,33 @@ export default async function GamePage({params}: { params: { gameId:string, id: {game.name}(ID:{gameId}) - - + + + + + + + + + + + - + diff --git a/app/(authenticated)/sports/[id]/page.tsx b/app/(authenticated)/sports/[id]/page.tsx index 2eb9fc7..9b77f72 100644 --- a/app/(authenticated)/sports/[id]/page.tsx +++ b/app/(authenticated)/sports/[id]/page.tsx @@ -10,12 +10,13 @@ export default async function SportPage({params}: { params: { id: string } }) { const sportId = parseInt(params.id, 10) const sport = await sportFactory().show(sportId) const games = await gameFactory().index() - const filteredGames = games.filter((game) => game.sportId == sportId) - const gameType = filteredGames.find(game => game)?.type; + const filteredGames = games + .filter((game) => game.sportId == sportId) + .filter((game) => game.type === "league") - return( + return ( - + 管理者のダッシュボード @@ -24,24 +25,20 @@ export default async function SportPage({params}: { params: { id: string } }) { {sport.name} - {gameType === "league" && ( - - - - - - )} - {gameType === "tournament" && ( - - - - - - )} + + + + + - + diff --git a/components/league/legacy/AddGameEntryDialog.tsx b/components/league/legacy/AddGameEntryDialog.tsx new file mode 100644 index 0000000..b99e0c7 --- /dev/null +++ b/components/league/legacy/AddGameEntryDialog.tsx @@ -0,0 +1,140 @@ +'use client' +import {AgGridReact} from 'ag-grid-react'; +import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid +import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid +import {ColDef, GridApi, GridReadyEvent, ModuleRegistry} from 'ag-grid-community'; +import {ClientSideRowModelModule} from 'ag-grid-community'; +import {Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography} from "@mui/material"; +import {Team} from "@/src/models/TeamModel"; +import {useState} from "react"; +import {useRouter} from "next/navigation"; +import {Game, gameFactory} from "@/src/models/GameModel"; + +ModuleRegistry.registerModules([ClientSideRowModelModule]); + +export type AddGameEntryDialogProps = { + game: Game + teams: Team[] + entries: Team[] +} + +// Row Data Interface +type IRow = { + id: number, + name: string, +} + +export default function AddGameEntryDialog(props: AddGameEntryDialogProps) { + const router = useRouter() + const height = 'calc(100vh - 230px)'; + const [gridApi, setGridApi] = useState | null>(null) + const [isOpen, setIsOpen] = useState(false) + // Column Definitions: Defines & controls grid columns. + const colDefs: ColDef[] = [ + { + field: "id", + headerName: "ID", + headerCheckboxSelection: true, + checkboxSelection: true, + showDisabledCheckboxes: true, + }, + { + field: "name", + headerName: "チーム名", + filter: true + }, + ] + + const rowData: IRow[] = props.teams + .filter((team) => !props.entries.some((entry) => entry.id === team.id)) + .map((team) => { + return { + id: team.id, + name: team.name, + } as IRow + }) + + const handleGridReady = (params: GridReadyEvent) => { + setGridApi(params.api) + } + + const handleSubmit = async () => { + if (!gridApi) { + alert("エラーが発生しました") + return + } + + const selectedRowIds = gridApi + .getSelectedRows() + .map((row) => row.id) + + if (selectedRowIds.length === 0) { + alert("チームを選択してください") + return + } + + // add team user + await gameFactory().addGameEntries(props.game.id, selectedRowIds) + + // refresh + router.refresh() + // close + setIsOpen(false) + } + + return ( + <> + + + setIsOpen(false)} + maxWidth={"md"} + fullWidth + > + + + エントリーの追加 + + + +
+ +
+
+ + + + +
+ + ) +} \ No newline at end of file diff --git a/components/league/legacy/ConfirmDialog.tsx b/components/league/legacy/ConfirmDialog.tsx new file mode 100644 index 0000000..3288f6a --- /dev/null +++ b/components/league/legacy/ConfirmDialog.tsx @@ -0,0 +1,49 @@ +'use client' +import React from "react"; +import {Button, Dialog, DialogActions, DialogContent} from "@mui/material"; +import {OverridableStringUnion} from "@mui/types"; +import {ButtonPropsColorOverrides} from "@mui/material/Button/Button"; + +type ConfirmDialogProps = { + open: boolean + onClose: VoidFunction + onConfirm: VoidFunction + confirmText: string + confirmColor: OverridableStringUnion< + 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning', + ButtonPropsColorOverrides + > + children: React.ReactNode +} + +export function ConfirmDialog(props: ConfirmDialogProps) { + + return ( + + + {props.children} + + + + + + + + ); +} \ No newline at end of file diff --git a/components/league/legacy/GameEditFields.tsx b/components/league/legacy/GameEditFields.tsx new file mode 100644 index 0000000..75b6371 --- /dev/null +++ b/components/league/legacy/GameEditFields.tsx @@ -0,0 +1,179 @@ +'use client' +import {RefObject} from "react"; +import { + FormControl, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + Stack, + TextField, + TextFieldProps +} from "@mui/material"; +import {Game} from "@/src/models/GameModel"; +import {Tag} from "@/src/models/TagModel"; + +export type GameEditFieldsProps = { + nameRef: RefObject + descriptionRef: RefObject + wightRef: RefObject + typeState: string + setTypeState: (type: string) => void + calculationTypeState: string + setCalculationTypeState: (type: string) => void + tags: Tag[] + tag: string + setTag: (id: string) => void + game?: Game +} + +export default function GameEditFields(props: GameEditFieldsProps) { + const handleGameTypeChange = (e: SelectChangeEvent) => { + props.setTypeState(e.target.value.toString()) + } + + const handleCalculationTypeChange = (e: SelectChangeEvent) => { + props.setCalculationTypeState(e.target.value.toString()) + } + + const handleTagChange = (e: SelectChangeEvent) => { + props.setTag(e.target.value.toString()) + } + + return ( + + + {/* name */} + + {/* description */} + + {/* weight */} + + {/* game type */} + + 大会形式 + + + + {/* calculation type */} + + 採点方式 + + + + {/* calculation type */} + + タグ + + + + ) +} diff --git a/components/league/legacy/GameEntryContent.tsx b/components/league/legacy/GameEntryContent.tsx new file mode 100644 index 0000000..f5d7fbe --- /dev/null +++ b/components/league/legacy/GameEntryContent.tsx @@ -0,0 +1,66 @@ +'use client' +import {Button, Link, TableCell, TableRow} from "@mui/material"; +import {useState} from "react"; +import {useRouter} from "next/navigation"; +import {Game, gameFactory} from "@/src/models/GameModel"; +import {Team} from "@/src/models/TeamModel"; +import {ConfirmDialog} from "@/components/league/legacy/ConfirmDialog"; + +export type GameEntryContentProps = { + game: Game + entryTeam: Team +} + +export function GameEntryContent(props: GameEntryContentProps) { + const router = useRouter() + const [isDeleteOpen, setIsDeleteOpen] = useState(false) + + const deleteEntry = async () => { + await gameFactory().removeGameEntry( + props.game.id, + props.entryTeam.id + ) + + // refresh + router.refresh() + } + + return ( + <> + + + + {props.entryTeam.id} + + + {props.entryTeam.name} + {props.entryTeam.description} + + + + + {/*delete*/} + setIsDeleteOpen(false)} + onConfirm={() => deleteEntry()} + confirmText={"削除"} + confirmColor={"error"} + > +

{props.entryTeam.name}を削除しますか?

+
+ + ) +} \ No newline at end of file diff --git a/components/league/legacy/GameEntryList.tsx b/components/league/legacy/GameEntryList.tsx new file mode 100644 index 0000000..22ac580 --- /dev/null +++ b/components/league/legacy/GameEntryList.tsx @@ -0,0 +1,59 @@ +'use client' +import {Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material"; +import {GameEntryContent} from "./GameEntryContent"; +import {Team} from "@/src/models/TeamModel"; +import {Game} from "@/src/models/GameModel"; + +export type GameEntryListProps = { + game: Game + entries: Team[] +} + +export function GameEntryList(props: GameEntryListProps) { + const gameEntryComponents = props.entries.map(entry => { + return ( + + ) + }) + + return ( + <> + + + + + + ID + + + 名前 + + + 説明 + + + アクション + + + + + {gameEntryComponents} + +
+
+ + ) +} diff --git a/components/league/legacy/GameForm.tsx b/components/league/legacy/GameForm.tsx new file mode 100644 index 0000000..8ff0137 --- /dev/null +++ b/components/league/legacy/GameForm.tsx @@ -0,0 +1,126 @@ +'use client' +import {Game, gameFactory} from "@/src/models/GameModel"; +import {FormEvent, useRef, useState} from "react"; +import {Button, Stack, TextFieldProps} from "@mui/material"; +import {Tag} from "@/src/models/TagModel"; +import {useRouter} from "next/navigation"; +import GameEditFields from "@/components/league/legacy/GameEditFields"; + +export type FormType = "create" | "edit" + +export type GameFormProps = { + formType: FormType + sportId: number + tags: Tag[] + game?: Game +} + +export default function GameForm(props: GameFormProps) { + const router = useRouter() + // ref + const nameRef = useRef(null) + const descriptionRef = useRef(null) + const wightRef = useRef(null) + // state + const [typeState, setTypeState] = useState(props.game?.type ?? '') + const [calculationTypeState, setCalculationTypeState] = useState(props.game?.calculationType ?? '') + const [tag, setTag] = useState(props.game?.tagId?.toString() ?? '') + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + + // weight invalid + if (isNaN(parseInt(wightRef.current?.value as string))) { + alert("重みは数値で入力してください。(0~100)") + return + } + + // type invalid + if (typeState !== "tournament" && typeState !== "league") { + alert("トーナメントもしくはリーグが選択可能です。") + return + } + + // calculationType invalid + if (calculationTypeState !== "total_score" && calculationTypeState !== "diff_score") { + alert("合計得点もしくは得失点差が選択可能です。") + return + } + + // tag invalid + if (tag === " ") { + alert("タグを指定してください。") + return + } + + + + if (props.formType === "create") { + const result = await gameFactory().create({ + name: nameRef.current?.value as string, + description: descriptionRef.current?.value as string, + sportId: props.sportId, + type: typeState, + calculationType: calculationTypeState, + weight: wightRef.current?.value as number, + tagId: parseInt(tag) + }) + + // redirect to game profile + router.push(`/sports/${props.sportId}/games/${result.id}`) + } else { + const id = props.game?.id + if (!id) return + + await gameFactory().update( + id, + { + name: nameRef.current?.value as string, + description: descriptionRef.current?.value as string, + sportId: props.sportId, + type: typeState, + calculationType: calculationTypeState, + weight: wightRef.current?.value as number, + tagId: parseInt(tag) + }) + } + + // refresh list + router.refresh() + } + + + return ( + + + + + + + ) +} diff --git a/components/match/inProgressMatchList.tsx b/components/match/inProgressMatchList.tsx index 62d5e34..357c709 100644 --- a/components/match/inProgressMatchList.tsx +++ b/components/match/inProgressMatchList.tsx @@ -20,7 +20,9 @@ export default async function InProgressMatchList() { }) // pick the first match - matchList.push(inProgressMatches[0]) + if (inProgressMatches[0]) { + matchList.push(inProgressMatches[0]) + } } return ( diff --git a/components/match/sportInProgressMatchList.tsx b/components/match/sportInProgressMatchList.tsx index c67e199..d5a2a66 100644 --- a/components/match/sportInProgressMatchList.tsx +++ b/components/match/sportInProgressMatchList.tsx @@ -25,7 +25,9 @@ export default async function SportInProgressMatchList(props: SportInProgressMat }) // pick the first match - matchList.push(inProgressMatches[0]) + if (inProgressMatches[0]) { + matchList.push(inProgressMatches[0]) + } } return (