Compare commits

...

11 Commits

Author SHA1 Message Date
Vivien DUFOUR 2ace144c6a fix conflicts
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR c383432b64 fix ./verify
continuous-integration/drone/push Build is passing Details
1 year ago
Vivien DUFOUR 8315b79ba1 can now share tactic with account
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR bd244c8dde push all my shareTactic work since 2 weeks, (can share and unshare tactic to team, draggable element for share, share icon on tactic hover to share to an account, just need to fix api tomorrow)
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR e962c17f4d can drag tactic to a team to share and unshare tactic on team panel
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR 588f580bf9 Can now share, unshare and open shared tactic
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR 86a25d18b2 fix home-page (need to fix unauthorized access to shared tactic)
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR 51f150a16a can share tactic to team or account and can unshare tactic to everyone (build will fail because need to modify home page)
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR 2beda1c7f5 can share tactic to team (test)
continuous-integration/drone/push Build is failing Details
1 year ago
Vivien DUFOUR fa81bd0702 conflicts
1 year ago
maxime.batista 41d431c1cb fixes
1 year ago

@ -3,7 +3,6 @@
object Account { object Account {
<u>id <u>id
name name
age
email email
phone_number phone_number
password_hash password_hash
@ -24,6 +23,9 @@ object Tactic {
<u>id <u>id
name name
creationDate creationDate
owner
content
courtType
} }
object Team { object Team {

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

@ -22,6 +22,7 @@ body {
margin: 0px; margin: 0px;
height: 100%; height: 100%;
background-color: var(--second-color); background-color: var(--second-color);
overflow: hidden;
} }
.data { .data {
@ -29,6 +30,7 @@ body {
background-color: var(--main-color); background-color: var(--main-color);
border-radius: 0.75cap; border-radius: 0.75cap;
color: var(--main-contrast-color); color: var(--main-contrast-color);
position: relative;
} }
.data:hover { .data:hover {

@ -23,7 +23,6 @@
border-collapse: separate; border-collapse: separate;
border-spacing: 1em; border-spacing: 1em;
table-layout: fixed; table-layout: fixed;
overflow: hidden;
} }
#body-personal-space td { #body-personal-space td {
@ -38,3 +37,25 @@
tbody p { tbody p {
text-align: center; text-align: center;
} }
.share-icon-container {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
padding: 5px;
}
.share-button {
cursor: pointer;
}
.share-icon-button {
background-color: transparent;
border: none;
}
.share-icon {
color: white;
font-size: 2em;
}

@ -16,6 +16,7 @@
#side-menu-content { #side-menu-content {
width: 90%; width: 90%;
} }
.titre-side-menu { .titre-side-menu {
border-bottom: var(--main-color) solid 3px; border-bottom: var(--main-color) solid 3px;
width: 100%; width: 100%;

@ -1,22 +1,68 @@
@import url(../style/theme/dark.css);
@import url(../style/template/header.css);
body {
margin: 0;
background-color: var(--second-color);
color: white;
}
#main-div { #main-div {
display: flex;
flex-direction: column;
align-items: stretch;
font-family: var(--font-content);
height: 100vh;
}
#content-container {
display: flex;
flex-direction: row;
align-items: stretch;
height: 90vh;
margin: 10px 2% 0 2%;
}
#left-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
height: 100%; width: 68%;
margin-right: 2%;
margin-bottom: 15px;
} }
#main-div header { #right-panel {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
flex-grow: 1;
}
header {
display: flex; display: flex;
justify-content: center; justify-content: center;
background-color: #525252; background-color: var(--main-color);
width: 100%; width: 100%;
margin-bottom: 5px; height: 80%;
padding-bottom: 10px;
padding-top: 10px;
} }
header h1 a { header h1 a {
color: orange; color: var(--accent-color);
text-decoration: none; text-decoration: none;
font-size: 1.5em; font-size: 1.4em;
}
html,
body,
#main-div,
#content-container,
#right-panel,
#tactics {
height: 100%;
} }
.square { .square {
@ -29,10 +75,10 @@ header h1 a {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 60%; width: 100%;
background-color: #8f8f8f;
padding-bottom: 10px; padding-bottom: 10px;
border-radius: 10px; border-radius: 10px;
background-color: var(--third-color);
} }
#first-part { #first-part {
@ -45,10 +91,19 @@ header h1 a {
font-size: 2.8em; font-size: 2.8em;
} }
#logo {
aspect-ratio: 3/2;
object-fit: contain;
max-width: 60%; /* Adjusted max-width for the team logo */
max-height: 60vh; /* Added max-height for the team logo */
margin-bottom: 10px; /* Added margin at the bottom */
}
#colors { #colors {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.color { .color {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
@ -69,13 +124,6 @@ header h1 a {
justify-content: space-around; justify-content: space-around;
} }
#logo {
aspect-ratio: 3/2;
object-fit: contain;
max-width: 70%;
max-height: 70%;
}
#delete { #delete {
border-radius: 10px; border-radius: 10px;
background-color: red; background-color: red;
@ -98,6 +146,7 @@ header h1 a {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-evenly; justify-content: space-evenly;
margin-bottom: 10px;
} }
#add-member { #add-member {
@ -110,8 +159,8 @@ header h1 a {
#members { #members {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #bcbcbc; background-color: var(--third-color);
width: 60%; width: 100%;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
border-radius: 10px; border-radius: 10px;
@ -119,7 +168,7 @@ header h1 a {
.member { .member {
width: 60%; width: 60%;
background-color: white; background-color: #494b5d;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-evenly; justify-content: space-evenly;
@ -133,3 +182,54 @@ header h1 a {
height: 40px; height: 40px;
width: 40px; width: 40px;
} }
#tactics {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--third-color);
width: 100%;
height: 100%;
border-radius: 10px;
overflow: auto;
margin-bottom: 5px;
}
#head-tactics {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-evenly;
margin-bottom: 10px;
}
.tactic {
box-sizing: border-box;
width: 100%;
background-color: #494b5d;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-radius: 10px;
margin-top: 5px;
margin-bottom: 5px;
padding: 10px;
max-width: 90%;
cursor: pointer;
}
.tactic button#open-tactic {
border-radius: 10px;
background-color: green;
color: white;
padding: 5px 8px;
}
.tactic button#delete {
border-radius: 10px;
background-color: red;
color: white;
padding: 5px 8px;
margin-left: 10px;
}

@ -1,8 +1,15 @@
import "../style/home/home.css" import "../style/home/home.css"
// import AccountSvg from "../assets/account.svg?react"
import { Header } from "./template/Header" import { Header } from "./template/Header"
import { BASE } from "../Constants" import { BASE } from "../Constants"
import Draggable from "react-draggable"
import { NULL_POS } from "../components/arrows/Pos"
import { contains } from "../components/arrows/Box"
import React, { useRef, useState } from "react"
import { fetchAPI } from "../Fetcher"
import { User } from "../model/User"
import { FaShare } from "react-icons/fa"
import { SaveStates } from "../components/editor/SavingState"
interface Tactic { interface Tactic {
id: number id: number
@ -22,20 +29,21 @@ export default function Home({
lastTactics, lastTactics,
allTactics, allTactics,
teams, teams,
username, user,
}: { }: {
lastTactics: Tactic[] lastTactics: Tactic[]
allTactics: Tactic[] allTactics: Tactic[]
teams: Team[] teams: Team[]
username: string user: User
}) { }) {
return ( return (
<div id="main"> <div id="main">
<Header username={username} /> <Header user={user} />
<Body <Body
lastTactics={lastTactics} lastTactics={lastTactics}
allTactics={allTactics} allTactics={allTactics}
teams={teams} teams={teams}
user={user}
/> />
</div> </div>
) )
@ -45,16 +53,23 @@ function Body({
lastTactics, lastTactics,
allTactics, allTactics,
teams, teams,
user,
}: { }: {
lastTactics: Tactic[] lastTactics: Tactic[]
allTactics: Tactic[] allTactics: Tactic[]
teams: Team[] teams: Team[]
user: User
}) { }) {
const widthPersonalSpace = 78 const widthPersonalSpace = 78
const widthSideMenu = 100 - widthPersonalSpace const widthSideMenu = 100 - widthPersonalSpace
return ( return (
<div id="body"> <div id="body">
<PersonalSpace width={widthPersonalSpace} allTactics={allTactics} /> <PersonalSpace
width={widthPersonalSpace}
allTactics={allTactics}
teams={teams}
user={user}
/>
<SideMenu <SideMenu
width={widthSideMenu} width={widthSideMenu}
lastTactics={lastTactics} lastTactics={lastTactics}
@ -80,8 +95,8 @@ function SideMenu({
width: width + "%", width: width + "%",
}}> }}>
<div id="side-menu-content"> <div id="side-menu-content">
<Team teams={teams} /> <TeamList teams={teams} />
<Tactic lastTactics={lastTactics} /> <TacticList lastTactics={lastTactics} />
</div> </div>
</div> </div>
) )
@ -90,9 +105,13 @@ function SideMenu({
function PersonalSpace({ function PersonalSpace({
width, width,
allTactics, allTactics,
teams,
user,
}: { }: {
width: number width: number
allTactics: Tactic[] allTactics: Tactic[]
teams: Team[]
user: User
}) { }) {
return ( return (
<div <div
@ -101,7 +120,11 @@ function PersonalSpace({
width: width + "%", width: width + "%",
}}> }}>
<TitlePersonalSpace /> <TitlePersonalSpace />
<BodyPersonalSpace allTactics={allTactics} /> <BodyPersonalSpace
allTactics={allTactics}
teams={teams}
user={user}
/>
</div> </div>
) )
} }
@ -114,7 +137,15 @@ function TitlePersonalSpace() {
) )
} }
function TableData({ allTactics }: { allTactics: Tactic[] }) { function TableData({
allTactics,
teams,
user,
}: {
allTactics: Tactic[]
teams: Team[]
user: User
}) {
const nbRow = Math.floor(allTactics.length / 3) + 1 const nbRow = Math.floor(allTactics.length / 3) + 1
let listTactic = Array(nbRow) let listTactic = Array(nbRow)
for (let i = 0; i < nbRow; i++) { for (let i = 0; i < nbRow; i++) {
@ -133,15 +164,12 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) {
i = 0 i = 0
while (i < nbRow) { while (i < nbRow) {
listTactic[i] = listTactic[i].map((tactic: Tactic) => ( listTactic[i] = listTactic[i].map((tactic: Tactic, index: number) => (
<td <DraggableTableDataElement
key={tactic.id} key={index}
className="data" tactic={tactic}
onClick={() => { teams={teams}
location.pathname = BASE + "/tactic/" + tactic.id + "/edit" />
}}>
{truncateString(tactic.name, 25)}
</td>
)) ))
i++ i++
} }
@ -159,12 +187,90 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) {
return data return data
} }
function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { function DraggableTableDataElement({
tactic,
teams,
}: {
tactic: Tactic
teams: Team[]
}) {
const ref = useRef<HTMLDivElement>(null)
const [dragging, setDragging] = useState(false)
const [hovered, setHovered] = useState(false)
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
if (!dragging) {
const userEmail = window.prompt(
"Entrez l'email à qui partager la tactique :",
)
if (userEmail != null) {
onShareTactic(userEmail, tactic)
}
} else {
setDragging(false)
}
}
return (
<Draggable
position={NULL_POS}
nodeRef={ref}
onDrag={() => setDragging(true)}
onStop={() => {
if (dragging) {
if (ref.current) {
onDropTactic(
ref.current.getBoundingClientRect(),
tactic,
teams,
)
}
}
}}>
<td
key={tactic.id}
ref={ref as React.RefObject<HTMLTableDataCellElement>}
className="data"
onClick={() => {
if (!dragging) {
location.pathname =
BASE + "/tactic/" + tactic.id + "/edit"
} else {
setDragging(false)
}
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}>
{truncateString(tactic.name, 25)}
{hovered && (
<div className="share-icon-container">
<button
className="share-button share-icon-button"
onClick={handleButtonClick}>
<FaShare className="share-icon" />
</button>
</div>
)}
</td>
</Draggable>
)
}
function BodyPersonalSpace({
allTactics,
teams,
user,
}: {
allTactics: Tactic[]
teams: Team[]
user: User
}) {
let data let data
if (allTactics.length == 0) { if (allTactics.length == 0) {
data = <p>Aucune tactique créée !</p> data = <p>Aucune tactique créée !</p>
} else { } else {
data = <TableData allTactics={allTactics} /> data = <TableData allTactics={allTactics} teams={teams} user={user} />
} }
return ( return (
@ -176,7 +282,7 @@ function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) {
) )
} }
function Team({ teams }: { teams: Team[] }) { function TeamList({ teams }: { teams: Team[] }) {
return ( return (
<div id="teams"> <div id="teams">
<div className="titre-side-menu"> <div className="titre-side-menu">
@ -192,7 +298,7 @@ function Team({ teams }: { teams: Team[] }) {
) )
} }
function Tactic({ lastTactics }: { lastTactics: Tactic[] }) { function TacticList({ lastTactics }: { lastTactics: Tactic[] }) {
return ( return (
<div id="tactic"> <div id="tactic">
<div className="titre-side-menu"> <div className="titre-side-menu">
@ -210,14 +316,16 @@ function Tactic({ lastTactics }: { lastTactics: Tactic[] }) {
} }
function SetButtonTactic({ tactics }: { tactics: Tactic[] }) { function SetButtonTactic({ tactics }: { tactics: Tactic[] }) {
const lastTactics = tactics.map((tactic) => ( const lastTactics = tactics.map((tactic, i) => (
<ButtonLastTactic tactic={tactic} /> <ButtonLastTactic key={i} tactic={tactic} />
)) ))
return <div className="set-button">{lastTactics}</div> return <div className="set-button">{lastTactics}</div>
} }
function SetButtonTeam({ teams }: { teams: Team[] }) { function SetButtonTeam({ teams }: { teams: Team[] }) {
const listTeam = teams.map((teams) => <ButtonTeam team={teams} />) const listTeam = teams.map((teams, i) => (
<ButtonTeam key={i} team={teams} />
))
return <div className="set-button">{listTeam}</div> return <div className="set-button">{listTeam}</div>
} }
@ -226,7 +334,7 @@ function ButtonTeam({ team }: { team: Team }) {
return ( return (
<div> <div>
<div <div
id={"button-team" + team.id} id={"button-team-" + team.id}
className="button-side-menu data" className="button-side-menu data"
onClick={() => { onClick={() => {
location.pathname = BASE + "/team/" + team.id location.pathname = BASE + "/team/" + team.id
@ -257,3 +365,63 @@ function truncateString(name: string, limit: number): string {
} }
return name return name
} }
function onDropTactic(ref: DOMRect, tactic: Tactic, teams: Team[]) {
let shared = false
for (const team of teams) {
if (
contains(
ref,
document
.getElementById(`button-team-${team.id}`)!
.getBoundingClientRect(),
)
) {
if (!shared) {
shareTacticToTeam(tactic, team)
shared = true
}
}
}
}
async function onShareTactic(email: string, tactic: Tactic) {
const canShareResponse = await fetchAPI(
`tactic/${tactic.id}/can-share`,
tactic,
)
if (canShareResponse.ok) {
const shareToAccountResponse = await fetchAPI(
`tactic/${tactic.id}/share-to-account`,
{ email },
)
if (!shareToAccountResponse.ok) {
alert(
"Une erreur s'est produite lors du partage de la tactique avec ce compte",
)
}
} else {
alert("Vous ne pouvez pas partager cette tactique")
}
}
async function shareTacticToTeam(tactic: Tactic, team: Team) {
const canShare = await fetchAPI(
`tactic/${tactic.id}/can-share-to-team`,
team,
).then((r) => r.ok)
if (
canShare &&
confirm(
"Etes-vous sûr de vouloir partager la tactique " +
tactic.name +
" avec l'équipe " +
team.name,
)
) {
fetchAPI(`tactic/${tactic.id}/share-to-team`, team)
}
if (!canShare) {
alert("Vous ne pouvez pas partager cette tactique à cette équipe")
}
}

@ -1,28 +1,44 @@
import "../style/team_panel.css" import "../style/team_panel.css"
import { BASE } from "../Constants" import { BASE } from "../Constants"
import { Team, TeamInfo, Member } from "../model/Team" import { Team, TeamInfo, Member } from "../model/Team"
import { User } from "../model/User" import { Tactic } from "../model/tactic/Tactic"
import { fetchAPI } from "../Fetcher"
import { useState } from "react"
export default function TeamPanel({ export default function TeamPanel({
isCoach, isCoach,
team, team,
currentUserId, currentUserId,
tactics,
}: { }: {
isCoach: boolean isCoach: boolean
team: Team team: Team
currentUserId: number currentUserId: number
tactics: Tactic[]
}) { }) {
const [teamTactics, setTeamTactics] = useState(tactics)
function handleTacticDelete(tacticId: number) {
fetchAPI(`tactic/${tacticId}/unshare-to-team`, team.info)
const updatedTactics = teamTactics.filter(
(tactic) => tactic.id !== tacticId,
)
setTeamTactics(updatedTactics)
}
return ( return (
<div id="main-div"> <div id="main-div">
<div id="header-div">
<header> <header>
<h1> <h1>
<a href={BASE + "/"}>IQBall</a> <a href={BASE + "/"}>IQBall</a>
</h1> </h1>
</header> </header>
</div>
<div id="content-container">
<div id="left-panel">
<TeamDisplay team={team.info} /> <TeamDisplay team={team.info} />
{isCoach && <CoachOptions id={team.info.id} />} {isCoach && <CoachOptions id={team.info.id} />}
<MembersDisplay <MembersDisplay
members={team.members} members={team.members}
isCoach={isCoach} isCoach={isCoach}
@ -30,6 +46,17 @@ export default function TeamPanel({
currentUserId={currentUserId} currentUserId={currentUserId}
/> />
</div> </div>
<div id="right-panel">
<TacticsDisplay
tactics={teamTactics}
isCoach={isCoach}
onTacticDelete={(tacticId) =>
handleTacticDelete(tacticId)
}
/>
</div>
</div>
</div>
) )
} }
@ -94,6 +121,7 @@ function MembersDisplay({
}) { }) {
const listMember = members.map((member) => ( const listMember = members.map((member) => (
<MemberDisplay <MemberDisplay
key={member.user.id}
member={member} member={member}
isCoach={isCoach} isCoach={isCoach}
idTeam={idTeam} idTeam={idTeam}
@ -167,3 +195,62 @@ function MemberDisplay({
</div> </div>
) )
} }
function TacticsDisplay({
tactics,
isCoach,
onTacticDelete,
}: {
tactics: Tactic[]
isCoach: boolean
onTacticDelete: (tacticId: number) => void
}) {
const listTactic = tactics.map((tactic) => (
<TacticDisplay
key={tactic.id}
tactic={tactic}
isCoach={isCoach}
onTacticDelete={() => onTacticDelete(tactic.id)}
/>
))
return (
<div id="tactics">
<div id="head-tactics">
<h1>Tactiques partagées</h1>
</div>
{listTactic}
</div>
)
}
function TacticDisplay({
tactic,
isCoach,
onTacticDelete,
}: {
tactic: Tactic
isCoach: boolean
onTacticDelete: () => void
}) {
return (
<div
className="tactic"
onClick={() =>
(location.pathname = BASE + "/tactic/" + tactic.id + "/edit")
}>
<p>{tactic.name}</p>
{isCoach && (
<button
id="delete"
onClick={(event) => {
event.stopPropagation()
confirm(
"Êtes-vous sûr de vouloir supprimer le partage cette tactique ?",
) && onTacticDelete()
}}>
Retirer
</button>
)}
</div>
)
}

@ -1,11 +1,12 @@
import { BASE } from "../../Constants" import { BASE } from "../../Constants"
import accountSvg from "../../assets/account.svg" import { User } from "../../model/User"
/** /**
* *
* @param param0 username * @param param0 username
* @returns Header * @returns Header
*/ */
export function Header({ username }: { username: string }) { export function Header({ user }: { user: User }) {
return ( return (
<div id="header"> <div id="header">
<div id="header-left"></div> <div id="header-left"></div>
@ -22,15 +23,16 @@ export function Header({ username }: { username: string }) {
</div> </div>
<div id="header-right"> <div id="header-right">
<div className="clickable" id="clickable-header-right"> <div className="clickable" id="clickable-header-right">
{/* <AccountSvg id="img-account" /> */}
<img <img
id="img-account" id="img-account"
src={accountSvg} src={user.profilePicture}
onClick={() => { onClick={() => {
location.pathname = BASE + "/settings" location.pathname = BASE + "/settings"
}} }}
alt="photo de profil"
/> />
<p id="username">{username}</p> <p id="username">{user.name}</p>
</div> </div>
</div> </div>
</div> </div>

@ -13,6 +13,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-draggable": "^4.4.6", "react-draggable": "^4.4.6",
"react-icons": "^5.0.1",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0", "vite": "^4.5.0",
"vite-plugin-css-injected-by-js": "^3.3.0" "vite-plugin-css-injected-by-js": "^3.3.0"

@ -17,12 +17,18 @@ use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Gateway\TacticInfoGateway;
use IQBall\Core\Model\AuthModel; use IQBall\Core\Model\AuthModel;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Gateway\TeamGateway;
use IQBall\Core\Model\TeamModel;
use IQBall\Core\Gateway\MemberGateway;
$basePath = get_base_path() . "/api"; $basePath = get_base_path() . "/api";
function getTacticController(): APITacticController { function getTacticController(): APITacticController {
return new APITacticController(new TacticModel(new TacticInfoGateway(new Connection(get_database())))); return new APITacticController(
new TacticModel(new TacticInfoGateway(new Connection(get_database())), new AccountGateway(new Connection(get_database()))),
new TeamModel(new TeamGateway(new Connection(get_database())), new MemberGateway(new Connection(get_database())), new AccountGateway(new Connection(get_database())))
);
} }
function getAuthController(): APIAuthController { function getAuthController(): APIAuthController {
@ -70,6 +76,14 @@ function getRoutes(): AltoRouter {
$router->map("POST", "/tactic/[i:id]/can-share", Action::auth(fn(int $id, Account $acc) => getTacticController()->canShareTactic($id, $acc)));
$router->map("POST", "/tactic/[i:id]/can-share-to-team", Action::auth(fn(int $id, Account $acc) => getTacticController()->canShareTacticToTeam($id, $acc)));
$router->map("POST", "/tactic/[i:id]/share-to-team", Action::auth(fn(int $id, Account $acc) => getTacticController()->shareTacticToTeam($id, $acc)));
$router->map("POST", "/tactic/[i:id]/share-to-account", Action::auth(fn(int $id, Account $acc) => getTacticController()->shareTacticToAccount($id, $acc)));
$router->map("POST", "/tactic/[i:id]/unshare-to-team", Action::auth(fn(int $id, Account $acc) => getTacticController()->unshareTacticToTeam($id, $acc)));
return $router; return $router;
} }

@ -11,6 +11,7 @@ use IQBall\App\Controller\EditorController;
use IQBall\App\Controller\TeamController; use IQBall\App\Controller\TeamController;
use IQBall\App\Controller\UserController; use IQBall\App\Controller\UserController;
use IQBall\App\Controller\VisualizerController; use IQBall\App\Controller\VisualizerController;
use IQBall\App\Controller\TacticController;
use IQBall\App\Session\MutableSessionHandle; use IQBall\App\Session\MutableSessionHandle;
use IQBall\App\Session\PhpSessionHandle; use IQBall\App\Session\PhpSessionHandle;
use IQBall\App\Session\SessionHandle; use IQBall\App\Session\SessionHandle;
@ -37,15 +38,15 @@ function getConnection(): Connection {
} }
function getUserController(): UserController { function getUserController(): UserController {
return new UserController(new TacticModel(new TacticInfoGateway(getConnection())), new TeamModel(new TeamGateway(getConnection()), new MemberGateway(getConnection()), new AccountGateway(getConnection()))); return new UserController(new TacticModel(new TacticInfoGateway(getConnection()), new AccountGateway(getConnection())), new TeamModel(new TeamGateway(getConnection()), new MemberGateway(getConnection()), new AccountGateway(getConnection())));
} }
function getVisualizerController(): VisualizerController { function getVisualizerController(): VisualizerController {
return new VisualizerController(new TacticModel(new TacticInfoGateway(getConnection()))); return new VisualizerController(new TacticModel(new TacticInfoGateway(getConnection()), new AccountGateway(getConnection())));
} }
function getEditorController(): EditorController { function getEditorController(): EditorController {
return new EditorController(new TacticModel(new TacticInfoGateway(getConnection()))); return new EditorController(new TacticModel(new TacticInfoGateway(getConnection()), new AccountGateway(getConnection())));
} }
function getTeamController(): TeamController { function getTeamController(): TeamController {
@ -53,6 +54,11 @@ function getTeamController(): TeamController {
return new TeamController(new TeamModel(new TeamGateway($con), new MemberGateway($con), new AccountGateway($con))); return new TeamController(new TeamModel(new TeamGateway($con), new MemberGateway($con), new AccountGateway($con)));
} }
function getTacticController(): TacticController {
return new TacticController(new TacticModel(new TacticInfoGateway(getConnection()), new AccountGateway(getConnection())), new TeamModel(new TeamGateway(getConnection()), new MemberGateway(getConnection()), new AccountGateway(getConnection())));
}
function getAuthController(): AuthController { function getAuthController(): AuthController {
return new AuthController(new AuthModel(new AccountGateway(getConnection()))); return new AuthController(new AuthModel(new AccountGateway(getConnection())));
} }
@ -86,6 +92,17 @@ function getRoutes(): AltoRouter {
$ar->map("GET", "/disconnect", Action::auth(fn(MutableSessionHandle $s) => getUserController()->disconnect($s))); $ar->map("GET", "/disconnect", Action::auth(fn(MutableSessionHandle $s) => getUserController()->disconnect($s)));
$ar->map("GET", "/shareTactic", Action::auth(fn(SessionHandle $s) => getTacticController()->displayTactic(true, $s)));
$ar->map("GET", "/shareTactic/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTacticController()->displayTeamAndAccount(true, $id, $s)));
$ar->map("GET", "/shareTactic/[i:id]/team/[i:idTeam]", Action::auth(fn(int $tacticId, int $teamId, SessionHandle $s) => getTacticController()->displayShareConfirmation($tacticId, $teamId, $s)));
$ar->map("POST", "/shareTactic/[i:id]/team/[i:idTeam]", Action::auth(fn(int $tacticId, int $teamId, SessionHandle $s) => getTacticController()->shareTacticToTeam($_POST, $tacticId, $teamId, $s)));
$ar->map("POST", "/shareTactic/[i:id]/account", Action::auth(fn(int $tacticId, SessionHandle $s) => getTacticController()->shareTacticToAccount($_POST, $tacticId, $s)));
$ar->map("GET", "/unshareTactic", Action::auth(fn(SessionHandle $s) => getTacticController()->displayTactic(false, $s)));
$ar->map("GET", "/unshareTactic/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTacticController()->displayTeamAndAccount(false, $id, $s)));
$ar->map("POST", "/unshareTactic/[i:id]", Action::auth(fn(int $id, SessionHandle $s) => getTacticController()->unshareTactic($id, $s)));
//tactic-related //tactic-related
$ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->openVisualizer($id, $s))); $ar->map("GET", "/tactic/[i:id]/view", Action::auth(fn(int $id, SessionHandle $s) => getVisualizerController()->openVisualizer($id, $s)));
$ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->openEditor($id, $s))); $ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->openEditor($id, $s)));
@ -95,6 +112,7 @@ function getRoutes(): AltoRouter {
$ar->map("GET", "/tactic/new/plain", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::plain(), $s))); $ar->map("GET", "/tactic/new/plain", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::plain(), $s)));
$ar->map("GET", "/tactic/new/half", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::half(), $s))); $ar->map("GET", "/tactic/new/half", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::half(), $s)));
//team-related //team-related
$ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); $ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s)));
$ar->map("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s))); $ar->map("POST", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->submitTeam($_POST, $s)));
@ -108,7 +126,6 @@ function getRoutes(): AltoRouter {
$ar->map("GET", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->displayEditTeam($idTeam, $s))); $ar->map("GET", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->displayEditTeam($idTeam, $s)));
$ar->map("POST", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->editTeam($idTeam, $_POST, $s))); $ar->map("POST", "/team/[i:id]/edit", Action::auth(fn(int $idTeam, SessionHandle $s) => getTeamController()->editTeam($idTeam, $_POST, $s)));
return $ar; return $ar;
} }

@ -4,6 +4,8 @@ DROP TABLE IF EXISTS Tactic;
DROP TABLE IF EXISTS Team; DROP TABLE IF EXISTS Team;
DROP TABLE IF EXISTS User; DROP TABLE IF EXISTS User;
DROP TABLE IF EXISTS Member; DROP TABLE IF EXISTS Member;
DROP TABLE IF EXISTS TacticSharedTeam;
DROP TABLE IF EXISTS TacticSharedAccount;
CREATE TABLE Admins CREATE TABLE Admins
( (
@ -45,6 +47,25 @@ CREATE TABLE Member
id_team integer NOT NULL, id_team integer NOT NULL,
id_user integer NOT NULL, id_user integer NOT NULL,
role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL, role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL,
PRIMARY KEY(id_team, id_user),
FOREIGN KEY (id_team) REFERENCES Team (id), FOREIGN KEY (id_team) REFERENCES Team (id),
FOREIGN KEY (id_user) REFERENCES Account (id) FOREIGN KEY (id_user) REFERENCES Account (id)
); );
CREATE TABLE TacticSharedTeam
(
id_team integer NOT NULL,
id_tactic integer NOT NULL,
PRIMARY KEY(id_team, id_tactic),
FOREIGN KEY (id_team) REFERENCES Team (id),
FOREIGN KEY (id_tactic) REFERENCES Tactic (id)
);
CREATE TABLE TacticSharedAccount
(
id_account integer NOT NULL,
id_tactic integer NOT NULL,
PRIMARY KEY(id_account, id_tactic),
FOREIGN KEY (id_account) REFERENCES Account (id),
FOREIGN KEY (id_tactic) REFERENCES Tactic (id)
);

@ -3,6 +3,7 @@
namespace IQBall\Api\Controller; namespace IQBall\Api\Controller;
use IQBall\Api\APIControl; use IQBall\Api\APIControl;
use IQBall\Core\Control;
use IQBall\Core\Data\Account; use IQBall\Core\Data\Account;
use IQBall\Core\Data\TacticInfo; use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpCodes;
@ -10,19 +11,25 @@ use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Validation\DefaultValidators; use IQBall\Core\Model\TeamModel;
use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\Validators;
/** /**
* API endpoint related to tactics * API endpoint related to tactics
*/ */
class APITacticController { class APITacticController {
private TacticModel $model; private TacticModel $tacticModel;
private TeamModel $teamModel;
/** /**
* @param TacticModel $model * @param TacticModel $tacticModel
* @param TeamModel $teamModel
*/ */
public function __construct(TacticModel $model) { public function __construct(TacticModel $tacticModel, TeamModel $teamModel) {
$this->model = $model; $this->tacticModel = $tacticModel;
$this->teamModel = $teamModel;
} }
/** /**
@ -36,7 +43,7 @@ class APITacticController {
"name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()], "name" => [DefaultValidators::lenBetween(1, 50), DefaultValidators::nameWithSpaces()],
], function (HttpRequest $request) use ($tactic_id, $account) { ], function (HttpRequest $request) use ($tactic_id, $account) {
$failures = $this->model->updateName($tactic_id, $request["name"], $account->getUser()->getId()); $failures = $this->tacticModel->updateName($tactic_id, $request["name"], $account->getUser()->getId());
if (!empty($failures)) { if (!empty($failures)) {
//TODO find a system to handle Unauthorized error codes more easily from failures. //TODO find a system to handle Unauthorized error codes more easily from failures.
@ -56,8 +63,7 @@ class APITacticController {
return APIControl::runChecked([ return APIControl::runChecked([
"content" => [], "content" => [],
], function (HttpRequest $req) use ($id) { ], function (HttpRequest $req) use ($id) {
//TODO verify that the account has the rights to update the tactic content if ($fail = $this->tacticModel->updateContent($id, json_encode($req["content"]))) {
if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) {
return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST);
} }
return HttpResponse::fromCode(HttpCodes::OK); return HttpResponse::fromCode(HttpCodes::OK);
@ -65,12 +71,99 @@ class APITacticController {
} }
/**
* @param int $tacticId
* @param Account $account
* @return HttpResponse
*/
public function canShareTactic(int $tacticId, Account $account): HttpResponse {
if ($this->tacticModel->canShareTactic($tacticId, $account)) {
return HttpResponse::fromCode(HttpCodes::OK);
}
return new JsonHttpResponse(["message" => "Vous ne pouvez pas partager cette tactique"], HttpCodes::FORBIDDEN);
}
/**
* @param int $tacticId
* @param Account $account
* @return HttpResponse
*/
public function canShareTacticToTeam(int $tacticId, Account $account): HttpResponse {
return Control::runChecked([
"id" => [],
"name" => [],
"picture" => [],
"main_color" => [],
"second_color" => [],
], function (HttpRequest $request) use ($tacticId, $account) {
if ($this->canShareTactic($tacticId, $account)->getCode() == HttpCodes::OK) {
if ($this->teamModel->canShareTacticToTeam($request["id"], $account->getUser()->getEmail())) {
return HttpResponse::fromCode(HttpCodes::OK);
}
}
return new JsonHttpResponse(["message" => "Action non autorisée"], HttpCodes::FORBIDDEN);
});
}
/**
* @param int $tacticId
* @param Account $account
* @return HttpResponse
*/
public function shareTacticToTeam(int $tacticId, Account $account): HttpResponse {
return Control::runChecked([
"id" => [],
"name" => [],
"picture" => [],
"main_color" => [],
"second_color" => [],
], function (HttpRequest $request) use ($tacticId) {
$this->teamModel->shareTacticToTeam($request["id"], $tacticId);
return HttpResponse::fromCode(HttpCodes::OK);
});
}
/**
* @param int $tacticId
* @param Account $account
* @return HttpResponse
*/
public function shareTacticToAccount(int $tacticId, Account $account): HttpResponse {
return Control::runChecked([
"email" => [],
], function (HttpRequest $request) use ($tacticId) {
$this->tacticModel->shareTacticToAccountMail($request["email"], $tacticId);
return HttpResponse::fromCode(HttpCodes::OK);
});
}
/**
* @param int $tacticId
* @param Account $account
* @return HttpResponse
*/
public function unshareTacticToTeam(int $tacticId, Account $account): HttpResponse {
return Control::runChecked([
"id" => [],
"name" => [],
"picture" => [],
"main_color" => [],
"second_color" => [],
], function (HttpRequest $request) use ($tacticId, $account) {
if ($this->teamModel->canShareTacticToTeam($request["id"], $account->getUser()->getEmail())) {
$this->teamModel->unshareTacticToTeam($tacticId, $request["id"]);
return HttpResponse::fromCode(HttpCodes::OK);
}
return new JsonHttpResponse(["message" => "Action non autorisée"], HttpCodes::FORBIDDEN);
});
}
/** /**
* @param int $userId * @param int $userId
* @return HttpResponse given user information. * @return HttpResponse given user information.
*/ */
public function getUserTactics(int $userId): HttpResponse { public function getUserTactics(int $userId): HttpResponse {
$tactics = $this->model->listAllOf($userId); $tactics = $this->tacticModel->listAllOf($userId);
$response = array_map(fn(TacticInfo $t) => [ $response = array_map(fn(TacticInfo $t) => [
'id' => $t->getId(), 'id' => $t->getId(),
@ -83,5 +176,4 @@ class APITacticController {
return new JsonHttpResponse($response); return new JsonHttpResponse($response);
} }
} }

@ -76,7 +76,15 @@ class EditorController {
public function openEditor(int $id, SessionHandle $session): ViewHttpResponse { public function openEditor(int $id, SessionHandle $session): ViewHttpResponse {
$tactic = $this->model->get($id); $tactic = $this->model->get($id);
$failure = TacticValidator::validateAccess($id, $tactic, $session->getAccount()->getUser()->getId()); $ownerId = $session->getAccount()->getUser()->getId();
$ids = $this->model->getOwnerIdTacticShared($id, $session->getAccount()->getUser()->getId());
if(isset($ids)) {
$ownerId = $ids['owner'];
}
$failure = TacticValidator::validateAccess($id, $tactic, $ownerId);
if ($failure != null) { if ($failure != null) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND); return ViewHttpResponse::twig('error.html.twig', ['failures' => [$failure]], HttpCodes::NOT_FOUND);

@ -0,0 +1,119 @@
<?php
namespace IQBall\App\Controller;
use IQBall\App\Session\SessionHandle;
use IQBall\App\ViewHttpResponse;
use IQBall\Core\Model\TacticModel;
use IQBall\Core\Model\TeamModel;
use IQBall\Core\Http\HttpResponse;
class TacticController {
private TacticModel $tactics;
private ?TeamModel $teams;
public function __construct(TacticModel $tactics, ?TeamModel $teams = null) {
$this->tactics = $tactics;
$this->teams = $teams;
}
/**
* @param bool $toShare
* @param SessionHandle $session
* @return ViewHttpResponse
*/
public function displayTactic(bool $toShare, SessionHandle $session): ViewHttpResponse {
if($toShare) {
$results = $this->tactics->getAll($session->getAccount()->getUser()->getId());
} else {
$results = $this->tactics->getAllTacticSharedOwned($session->getAccount()->getUser()->getId());
}
return ViewHttpResponse::twig("display_tactic.html.twig", ['tactics' => $results, 'toShare' => $toShare]);
}
/**
* @param bool $toShare
* @param int $tacticId
* @param SessionHandle $session
* @return ViewHttpResponse
*/
public function displayTeamAndAccount(bool $toShare, int $tacticId, SessionHandle $session): ViewHttpResponse {
if($toShare) {
$results = $this->teams->getAllIsCoach($session->getAccount()->getUser()->getId());
} else {
$results = $this->teams->getAllIsCoach($session->getAccount()->getUser()->getId());
}
return ViewHttpResponse::twig("display_user_teams_accounts.html.twig", ['teams' => $results, 'tactic' => $tacticId, 'toShare' => $toShare]);
}
/**
* @param int $tacticId
* @param int $teamId
* @param SessionHandle $session
* @return ViewHttpResponse
*/
public function displayShareConfirmation(int $tacticId, int $teamId, SessionHandle $session): ViewHttpResponse {
return ViewHttpResponse::twig("display_share_confirmation.html.twig", ['team' => $teamId, 'tactic' => $tacticId]);
}
/**
* @param array<array<string, mixed>> $confirmation
* @param int $tacticId
* @param int $teamId
* @param SessionHandle $session
* @return HttpResponse
*/
public function shareTacticToTeam(array $confirmation, int $tacticId, int $teamId, SessionHandle $session): HttpResponse {
if($confirmation['confirmation'] == "yes") {
$this->teams->shareTacticToTeam($teamId, $tacticId);
}
return ViewHttpResponse::redirect("/");
}
/**
* @param array<string> $request
* @param int $tacticId
* @param SessionHandle $session
* @return HttpResponse
*/
public function shareTacticToAccount(array $request, int $tacticId, SessionHandle $session): HttpResponse {
$email = $request["email"];
$this->tactics->shareTacticToAccountMail($email, $tacticId);
return ViewHttpResponse::redirect("/");
}
/**
* @param int $tacticId
* @param SessionHandle $session
* @return HttpResponse
*/
public function unshareTactic(int $tacticId, SessionHandle $session): HttpResponse {
$this->tactics->unshareTactic($tacticId);
$this->teams->unshareTactic($tacticId);
return ViewHttpResponse::redirect("/");
}
/**
* @param int $tacticId
* @param int $teamId
* @param SessionHandle $session
* @return HttpResponse
*/
public function unshareTacticToTeam(int $tacticId, int $teamId, SessionHandle $session): HttpResponse {
$this->teams->unshareTacticToTeam($tacticId, $teamId);
return ViewHttpResponse::redirect("/");
}
/**
* @param int $tacticId
* @param int $accountId
* @param SessionHandle $session
* @return HttpResponse
*/
public function unshareTacticToAccount(int $tacticId, int $accountId, SessionHandle $session): HttpResponse {
$this->tactics->unshareTacticToAccount($tacticId, $accountId);
return ViewHttpResponse::redirect("/");
}
}

@ -129,6 +129,7 @@ class TeamController {
], HttpCodes::FORBIDDEN); ], HttpCodes::FORBIDDEN);
} }
$role = $this->model->isCoach($id, $session->getAccount()->getUser()->getEmail()); $role = $this->model->isCoach($id, $session->getAccount()->getUser()->getEmail());
$tactics = $this->model->getAllTeamTactic($id);
return ViewHttpResponse::react( return ViewHttpResponse::react(
'views/TeamPanel.tsx', 'views/TeamPanel.tsx',
@ -138,7 +139,9 @@ class TeamController {
"members" => $result->listMembers(), "members" => $result->listMembers(),
], ],
'isCoach' => $role, 'isCoach' => $role,
'currentUserId' => $session->getAccount()->getUser()->getId()] 'currentUserId' => $session->getAccount()->getUser()->getId(),
'tactics' => $tactics,
]
); );
} }
@ -243,4 +246,10 @@ class TeamController {
$this->model->editTeam($idTeam, $request['name'], $request['picture'], $request['main_color'], $request['second_color']); $this->model->editTeam($idTeam, $request['name'], $request['picture'], $request['main_color'], $request['second_color']);
return HttpResponse::redirect('/team/' . $idTeam); return HttpResponse::redirect('/team/' . $idTeam);
} }
public function shareTactic(int $teamId, int $tacticId, SessionHandle $session): ViewHttpResponse {
$this->model->shareTacticToTeam($teamId, $tacticId);
return $this->displayTeam($teamId, $session);
}
} }

@ -35,7 +35,15 @@ class UserController {
$lastTactics = $this->tactics->getLast($limitNbTactics, $user->getId()); $lastTactics = $this->tactics->getLast($limitNbTactics, $user->getId());
$allTactics = $this->tactics->getAll($user->getId()); $allTactics = $this->tactics->getAll($user->getId());
$name = $user->getName();
$allTacticsShared = $this->tactics->getAllTacticShared($user->getId());
if(isset($allTacticsShared)) {
foreach ($allTacticsShared as $tactic) {
if(!in_array($tactic, $allTactics)) {
array_push($allTactics, $tactic);
}
}
}
if ($this->teams != null) { if ($this->teams != null) {
$teams = $this->teams->getAll($user->getId()); $teams = $this->teams->getAll($user->getId());
@ -47,7 +55,7 @@ class UserController {
"lastTactics" => $lastTactics, "lastTactics" => $lastTactics,
"allTactics" => $allTactics, "allTactics" => $allTactics,
"teams" => $teams, "teams" => $teams,
"username" => $name, "user" => $user,
]); ]);
} }
@ -62,5 +70,4 @@ class UserController {
$session->destroy(); $session->destroy();
return HttpResponse::redirect("/"); return HttpResponse::redirect("/");
} }
} }

@ -15,5 +15,4 @@ class TacticValidator {
} }
return null; return null;
} }
} }

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Confirmation</title>
</head>
<body>
<div class="container">
<h2>Etes-vous sûr de vouloir partager la tactique ?</h2>
<form action="{{ path("/shareTactic/#{tactic}/team/#{team}") }}" method="POST">
<div>
<input type="radio" id="yes" name="confirmation" value="yes" />
<label for="yes">Oui</label>
</div>
<div>
<input type="radio" id="no" name="confirmation" value="no" />
<label for="no">Non</label>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
</div>
</body>
</html>

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tactiques partageables</title>
</head>
<body>
<header>
<h1><a href="{{ path('/') }}">IQBall</a></h1>
</header>
{% if toShare %}
{% for t in tactics %}
<div onclick="window.location.href = '{{ path("/shareTactic/#{t.id}") }}'">
<p> {{ t.name }} </p>
</div>
{% endfor %}
{% else %}
{% for t in tactics %}
<div onclick="window.location.href = '{{ path("/unshareTactic/#{t.id}") }}'">
<p> {{ t.name }} </p>
</div>
{% endfor %}
{% endif %}
</body>
</html>

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Twig view</title>
</head>
<body>
{% if toShare %}
{% if teams is empty %}
<p>Vous n'êtes dans aucune équipe</p>
{% else %}
{% for team in teams %}
<div class="team" onclick="window.location.href = '{{ path("/shareTactic/#{tactic}/team/#{team.id}") }}'">
<p>Nom de l'équipe : {{ team.name }}</p>
<img src="{{ team.picture }}" alt="logo de l'équipe">
</div>
{% endfor %}
{% endif %}
<h2>Partager à un compte</h2>
<form action="{{ path("/shareTactic/#{tactic}/account") }}" method="POST">
<div class="form-group">
<label for="email">Email du membre :</label>
{% if badEmail %}
<p class="failed">Email invalide</p>
{% endif %}
{%if notFound %}
<p class="failed">Cette personne n'a pas été trouvé</p>
{% endif %}
<input type="text" id="email" name="email" required>
</div>
<div class="form-group">
<input type="submit" value="Confirmer">
</div>
</form>
{% else %}
<form action="{{ path("/unshareTactic/#{tactic}") }}" method="POST">
<button name="unshareAll" value="unshareAll">Supprimer le partage de cette tactique à tout le monde</button>
</form>
{% endif %}
</body>
</html>

@ -31,6 +31,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
margin: 0; margin: 0;
overflow: visible;
} }
</style> </style>

@ -2,7 +2,7 @@
namespace IQBall\Core\Data; namespace IQBall\Core\Data;
class TacticInfo { class TacticInfo implements \JsonSerializable {
private int $id; private int $id;
private string $name; private string $name;
private int $creationDate; private int $creationDate;
@ -59,4 +59,8 @@ class TacticInfo {
public function getCreationDate(): int { public function getCreationDate(): int {
return $this->creationDate; return $this->creationDate;
} }
public function jsonSerialize() {
return get_object_vars($this);
}
} }

@ -17,6 +17,14 @@ class AccountGateway {
$this->con = $con; $this->con = $con;
} }
/**
* @param string $name
* @param string $email
* @param string $token
* @param string $hash
* @param string $profilePicture
* @return int
*/
public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int { public function insertAccount(string $name, string $email, string $token, string $hash, string $profilePicture): int {
$this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profile_pic)", [ $this->con->exec("INSERT INTO Account(username, hash, email, token,profile_picture) VALUES (:username,:hash,:email,:token,:profile_pic)", [
':username' => [$name, PDO::PARAM_STR], ':username' => [$name, PDO::PARAM_STR],
@ -107,6 +115,19 @@ class AccountGateway {
return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"]))); return new Account($acc["token"], new User($email, $acc["username"], $acc["id"], $acc["profile_picture"], $this->isAdmin($acc["id"])));
} }
/**
* @param string $email
* @return array<array<string, mixed>>
*/
public function getAccountIdFromMail(string $email): array {
return $this->con->fetch(
"SELECT id FROM Account WHERE email = :mail",
[
":mail" => [$email, PDO::PARAM_STR],
]
);
}
/** /**
* @param string $token get an account from given token * @param string $token get an account from given token
* @return Account|null * @return Account|null
@ -158,6 +179,52 @@ class AccountGateway {
} }
/**
* @param int $accountId
* @param int $tacticId
* @return int
*/
public function shareTacticToAccount(int $accountId, int $tacticId): int {
$this->con->exec(
"INSERT INTO TacticSharedAccount(id_account, id_tactic) VALUES(:accountId, :tacticId)",
[
":accountId" => [$accountId, PDO::PARAM_INT],
":tacticId" => [$tacticId, PDO::PARAM_INT],
]
);
return intval($this->con->lastInsertId());
}
/**
* @param int $tacticId
* @return int
*/
public function unshareTactic(int $tacticId): int {
$this->con->exec(
"DELETE FROM TacticSharedAccount WHERE id_tactic = :tacticId",
[
":tacticId" => [$tacticId, PDO::PARAM_INT],
]
);
return intval($this->con->lastInsertId());
}
/**
* @param int $tacticId
* @param int $accountId
* @return int
*/
public function unshareTacticToAccount(int $tacticId, int $accountId): int {
$this->con->exec(
"DELETE FROM TacticSharedAccount WHERE id_tactic = :tacticId AND id_account = :accountId",
[
":tacticId" => [$tacticId, PDO::PARAM_INT],
":accountId" => [$accountId, PDO::PARAM_INT],
]
);
return intval($this->con->lastInsertId());
}
/** /**
* returns the total amount of accounts in the database * returns the total amount of accounts in the database
* @return int * @return int

@ -7,6 +7,8 @@ use IQBall\Core\Data\CourtType;
use IQBall\Core\Data\TacticInfo; use IQBall\Core\Data\TacticInfo;
use PDO; use PDO;
use function PHPStan\dumpType;
class TacticInfoGateway { class TacticInfoGateway {
private Connection $con; private Connection $con;
@ -43,9 +45,9 @@ class TacticInfoGateway {
* Return the nb last tactics created * Return the nb last tactics created
* *
* @param integer $nb * @param integer $nb
* @return array<array<string,mixed>> * @return TacticInfo[] | null
*/ */
public function getLast(int $nb, int $ownerId): ?array { public function getLastOwnedBy(int $nb, int $ownerId): ?array {
$res = $this->con->fetch( $res = $this->con->fetch(
"SELECT * "SELECT *
FROM Tactic FROM Tactic
@ -59,15 +61,15 @@ class TacticInfoGateway {
if (count($res) == 0) { if (count($res) == 0) {
return []; return [];
} }
return $res; return array_map(fn(array $row) => $this->rowToTacticInfo($row), $res);
} }
/** /**
* Get all the tactics of the owner * Get all the tactics of the owner
* *
* @return array<array<string,mixed>> * @return TacticInfo[] | null
*/ */
public function getAll(int $ownerId): ?array { public function getAllOwnedBy(int $ownerId): ?array {
$res = $this->con->fetch( $res = $this->con->fetch(
"SELECT * "SELECT *
FROM Tactic FROM Tactic
@ -80,7 +82,119 @@ class TacticInfoGateway {
if (count($res) == 0) { if (count($res) == 0) {
return []; return [];
} }
return $res; return array_map(fn(array $row) => $this->rowToTacticInfo($row), $res);
}
/**
* @param int $ownerId
* @return TacticInfo[]|null
*/
public function getAllTacticSharedTeam(int $ownerId): ?array {
$res = $this->con->fetch(
"SELECT t.* FROM Tactic t, TacticSharedTeam ts, Member m
WHERE ts.id_team = m.id_team AND ts.id_tactic = t.id
AND m.id_user = :ownerId",
[
":ownerId" => [$ownerId, PDO::PARAM_INT],
]
);
if (count($res) == 0) {
return [];
}
return array_map(fn(array $row) => $this->rowToTacticInfo($row), $res);
}
/**
* @param int $ownerId
* @return TacticInfo[]|null
*/
public function getAllTacticSharedAccount(int $ownerId): ?array {
$res = $this->con->fetch(
"SELECT t.* FROM Tactic t, TacticSharedAccount ta
WHERE t.id = ta.id_tactic AND ta.id_account = :ownerId",
[
":ownerId" => [$ownerId, PDO::PARAM_INT],
]
);
if (count($res) == 0) {
return [];
}
return array_map(fn(array $row) => $this->rowToTacticInfo($row), $res);
}
/**
* @param int $ownerId
* @return array<TacticInfo>|null
*/
public function getAllTacticSharedTeamOwned(int $ownerId): ?array {
$res = $this->con->fetch(
"SELECT t.* FROM Tactic t, TacticSharedTeam ts, Member m
WHERE ts.id_team = m.id_team AND ts.id_tactic = t.id
AND m.id_user = :ownerId AND t.owner = :ownerId",
[
":ownerId" => [$ownerId, PDO::PARAM_INT],
]
);
if (count($res) == 0) {
return [];
}
return array_map(fn(array $row) => $this->rowToTacticInfo($row), $res);
}
/**
* @param int $ownerId
* @return array<TacticInfo>|null
*/
public function getAllTacticSharedAccountOwned(int $ownerId): ?array {
$res = $this->con->fetch(
"SELECT t.* FROM Tactic t, TacticSharedAccount ta
WHERE t.id = ta.id_tactic AND t.owner = :ownerId",
[
":ownerId" => [$ownerId, PDO::PARAM_INT],
]
);
if (count($res) == 0) {
return [];
}
return array_map(fn(array $row) => $this->rowToTacticInfo($row), $res);
}
/**
* @param int $tacticId
* @param int $accountId
* @return array<array<string, mixed>>|null
*/
public function getOwnerIdTacticSharedTeam(int $tacticId, int $accountId): ?array {
return $this->con->fetch(
"SELECT t.owner
FROM Account a
JOIN Member m on a.id = m.id_user
JOIN TacticSharedTeam tt ON m.id_team = tt.id_team
JOIN Tactic t ON tt.id_tactic = t.id
WHERE tt.id_tactic = :tacticId AND m.id_user = :accountId",
[
":tacticId" => [$tacticId, PDO::PARAM_INT],
":accountId" => [$accountId, PDO::PARAM_INT],
]
);
}
/**
* @param int $tacticId
* @param int $accountId
* @return array<array<string, mixed>>|null
*/
public function getOwnerIdTacticSharedAccount(int $tacticId, int $accountId): ?array {
return $this->con->fetch(
"SELECT t.owner FROM Account a
JOIN TacticSharedAccount ta ON a.id = ta.id_account
JOIN Tactic t ON ta.id_tactic = t.id
WHERE ta.id_tactic = :tacticId AND ta.id_account = :accountId",
[
":tacticId" => [$tacticId, PDO::PARAM_INT],
":accountId" => [$accountId, PDO::PARAM_INT],
]
);
} }
@ -148,4 +262,13 @@ class TacticInfoGateway {
]); ]);
return $stmnt->rowCount() == 1; return $stmnt->rowCount() == 1;
} }
/**
* @param array<mixed> $row
* @return TacticInfo
*/
private function rowToTacticInfo(array $row): TacticInfo {
$type = CourtType::fromName($row['court_type']);
return new TacticInfo($row['id'], $row["name"], strtotime($row["creation_date"]), $row["owner"], $type, $row['content']);
}
} }

@ -3,8 +3,7 @@
namespace IQBall\Core\Gateway; namespace IQBall\Core\Gateway;
use IQBall\Core\Connection; use IQBall\Core\Connection;
use IQBall\Core\Data\Color; use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Data\Team;
use IQBall\Core\Data\TeamInfo; use IQBall\Core\Data\TeamInfo;
use PDO; use PDO;
@ -35,6 +34,53 @@ class TeamGateway {
return intval($this->con->lastInsertId()); return intval($this->con->lastInsertId());
} }
/**
* @param int $teamId
* @param int $tacticId
* @return int
*/
public function shareTacticToTeam(int $teamId, int $tacticId): int {
$this->con->exec(
"INSERT INTO TacticSharedTeam(id_team, id_tactic) VALUES(:teamId, :tacticId)",
[
":teamId" => [$teamId, PDO::PARAM_INT],
":tacticId" => [$tacticId, PDO::PARAM_INT],
]
);
return intval($this->con->lastInsertId());
}
/**
* @param int $tacticId
* @return int
*/
public function unshareTactic(int $tacticId): int {
$this->con->exec(
"DELETE FROM TacticSharedTeam WHERE id_tactic = :tacticId",
[
":tacticId" => [$tacticId, PDO::PARAM_INT],
]
);
return intval($this->con->lastInsertId());
}
/**
* @param int $tacticId
* @param int $teamId
* @return int
*/
public function unshareTacticToTeam(int $tacticId, int $teamId): int {
$this->con->exec(
"DELETE FROM TacticSharedTeam WHERE id_tactic = :tacticId AND id_team = :teamId",
[
":tacticId" => [$tacticId, PDO::PARAM_INT],
":teamId" => [$teamId, PDO::PARAM_INT],
]
);
return intval($this->con->lastInsertId());
}
/** /**
* @param string $name * @param string $name
* @param int $id * @param int $id
@ -140,6 +186,32 @@ class TeamGateway {
); );
} }
/**
* @param int $user
* @return array<array<string, mixed>>
*/
public function getAllIsCoach(int $user): array {
return $this->con->fetch(
"SELECT t.* FROM team t,Member m WHERE m.id_team = t.id AND m.id_user= :idUser AND m.role = 'COACH'",
[
"idUser" => [$user, PDO::PARAM_INT],
]
);
}
/**
* @param int $team
* @return array<array<string, mixed>>
*/
public function getAllTeamTactic(int $team): array {
return $this->con->fetch(
"SELECT t.* FROM Tactic t, TacticSharedTeam tt WHERE t.id = tt.id_tactic AND tt.id_team = :idTeam",
[
"idTeam" => [$team, PDO::PARAM_INT],
]
);
}
/** /**
* @param int $start * @param int $start
* @param int $n * @param int $n
@ -182,5 +254,4 @@ class TeamGateway {
} }
} }
} }

@ -2,9 +2,11 @@
namespace IQBall\Core\Model; namespace IQBall\Core\Model;
use IQBall\Core\Data\Account;
use IQBall\Core\Data\CourtType; use IQBall\Core\Data\CourtType;
use IQBall\App\Session\SessionHandle; use IQBall\App\Session\SessionHandle;
use IQBall\Core\Data\TacticInfo; use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Gateway\AccountGateway;
use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Gateway\TacticInfoGateway;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
@ -12,12 +14,14 @@ class TacticModel {
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
private TacticInfoGateway $tactics; private TacticInfoGateway $tactics;
private AccountGateway $users;
/** /**
* @param TacticInfoGateway $tactics * @param TacticInfoGateway $tactics
*/ */
public function __construct(TacticInfoGateway $tactics) { public function __construct(TacticInfoGateway $tactics, AccountGateway $users) {
$this->tactics = $tactics; $this->tactics = $tactics;
$this->users = $users;
} }
/** /**
@ -56,10 +60,10 @@ class TacticModel {
* *
* @param integer $nb * @param integer $nb
* @param integer $ownerId * @param integer $ownerId
* @return array<array<string,mixed>> * @return array<TacticInfo>
*/ */
public function getLast(int $nb, int $ownerId): array { public function getLast(int $nb, int $ownerId): array {
return $this->tactics->getLast($nb, $ownerId); return $this->tactics->getLastOwnedBy($nb, $ownerId);
} }
@ -78,11 +82,159 @@ class TacticModel {
* Get all the tactics of the owner * Get all the tactics of the owner
* *
* @param integer $ownerId * @param integer $ownerId
* @return array<array<string,mixed>> * @return array<TacticInfo>|null
*/ */
public function getAll(int $ownerId): ?array { public function getAll(int $ownerId): ?array {
return $this->tactics->getAll($ownerId); return $this->tactics->getAllOwnedBy($ownerId);
} }
/**
* Get all the tactics shared to a user via team share
* @param int $ownerId
* @return array<TacticInfo>|null
*/
public function getAllTacticSharedTeam(int $ownerId): ?array {
return $this->tactics->getAllTacticSharedTeam($ownerId);
}
/**
* Get all the tactics shared to a user via account share
* @param int $ownerId
* @return array<TacticInfo>|null
*/
public function getAllTacticSharedAccount(int $ownerId): ?array {
return $this->tactics->getAllTacticSharedAccount($ownerId);
}
/**
* Get all the tactics shared to a user via team or account share
* @param int $ownerId
* @return array<TacticInfo>|null
*/
public function getAllTacticShared(int $ownerId): ?array {
$allTactics = [];
$allTacticsSharedTeam = $this->tactics->getAllTacticSharedTeam($ownerId);
if(isset($allTacticsSharedTeam)) {
foreach ($allTacticsSharedTeam as $tactic) {
if(!in_array($tactic, $allTactics)) {
array_push($allTactics, $tactic);
}
}
}
$allTacticsSharedAccount = $this->tactics->getAllTacticSharedAccount($ownerId);
if(isset($allTacticsSharedAccount)) {
foreach ($allTacticsSharedAccount as $tactic) {
if(!in_array($tactic, $allTactics)) {
array_push($allTactics, $tactic);
}
}
}
return $allTactics;
}
/**
* Get all the tactics a user shared and own
* @param int $ownerId
* @return array<TacticInfo>|null
*/
public function getAllTacticSharedOwned(int $ownerId): ?array {
$allTactics = [];
$allTacticsSharedTeamOwned = $this->tactics->getAllTacticSharedTeamOwned($ownerId);
if(isset($allTacticsSharedTeamOwned)) {
foreach ($allTacticsSharedTeamOwned as $tactic) {
if(!in_array($tactic, $allTactics)) {
array_push($allTactics, $tactic);
}
}
}
$allTacticsSharedAccountOwned = $this->tactics->getAllTacticSharedAccountOwned($ownerId);
if(isset($allTacticsSharedAccountOwned)) {
foreach ($allTacticsSharedAccountOwned as $tactic) {
if(!in_array($tactic, $allTactics)) {
array_push($allTactics, $tactic);
}
}
}
return $allTactics;
}
/**
* Share tactic to an account
* @param int $accountId
* @param int $tacticId
* @return int
*/
public function shareTacticToAccount(int $accountId, int $tacticId): int {
return $this->users->shareTacticToAccount($accountId, $tacticId);
}
/**
* Share tactic to an account email
* @param string $email
* @param int $tacticId
* @return int|null
*/
public function shareTacticToAccountMail(string $email, int $tacticId): ?int {
$account = $this->users->getAccountFromMail($email);
$accountId = $account->getUser()->getId();
return $this->shareTacticToAccount($accountId, $tacticId);
}
/**
* Unshare a tactic to every account
* @param int $tacticId
* @return int
*/
public function unshareTactic(int $tacticId): int {
return $this->users->unshareTactic($tacticId);
}
/**
* Unshare a tactic to an account
* @param int $tacticId
* @param int $accountId
* @return int
*/
public function unshareTacticToAccount(int $tacticId, int $accountId): int {
return $this->users->unshareTacticToAccount($tacticId, $accountId);
}
/**
* Get the owner of a shared tactic
* @param int $tactidId
* @param int $accountId
* @return array<int>|null
*/
public function getOwnerIdTacticShared(int $tactidId, int $accountId): ?array {
$ids = [];
$idT = $this->tactics->getOwnerIdTacticSharedTeam($tactidId, $accountId);
if(isset($idT)) {
foreach ($idT as $id) {
if(!in_array($id, $ids)) {
array_push($ids, $id);
}
}
}
$idA = $this->tactics->getOwnerIdTacticSharedAccount($tactidId, $accountId);
if(isset($idA)) {
foreach ($idA as $id) {
if(!in_array($id, $ids)) {
array_push($ids, $id);
}
}
}
if($ids != null) {
return $ids[0];
}
return null;
}
/** /**
* Update the name of a tactic * Update the name of a tactic
* @param int $id the tactic identifier * @param int $id the tactic identifier
@ -107,6 +259,7 @@ class TacticModel {
return []; return [];
} }
public function updateContent(int $id, string $json): ?ValidationFail { public function updateContent(int $id, string $json): ?ValidationFail {
if (!$this->tactics->updateContent($id, $json)) { if (!$this->tactics->updateContent($id, $json)) {
return ValidationFail::error("Could not update content"); return ValidationFail::error("Could not update content");
@ -114,4 +267,16 @@ class TacticModel {
return null; return null;
} }
/**
* Verify if tactic can be shared by user
* @param int $id
* @param Account $account
* @return bool
*/
public function canShareTactic(int $id, Account $account): bool {
if($this->get($id)->getOwnerId() != $account->getUser()->getId()) {
return false;
}
return true;
}
} }

@ -78,6 +78,48 @@ class TeamModel {
return new Team($teamInfo, $members); return new Team($teamInfo, $members);
} }
/**
* Share tactic to a team
* @param int $teamId
* @param int $tacticId
* @return int
*/
public function shareTacticToTeam(int $teamId, int $tacticId): int {
return $this->teams->shareTacticToTeam($teamId, $tacticId);
}
/**
* Unshare tactic from every team
* @param int $tacticId
* @return int
*/
public function unshareTactic(int $tacticId): int {
return $this->teams->unshareTactic($tacticId);
}
/**
* Unshare team-shared tactic
* @param int $tacticId
* @param int $teamId
* @return int
*/
public function unshareTacticToTeam(int $tacticId, int $teamId): int {
return $this->teams->unshareTacticToTeam($tacticId, $teamId);
}
/**
* Verify if user can share tactic to a team
* @param int $teamId
* @param string $email
* @return bool
*/
public function canShareTacticToTeam(int $teamId, string $email): bool {
if($this->isCoach($teamId, $email)) {
return true;
}
return false;
}
/** /**
* delete a member from given team identifier * delete a member from given team identifier
* @param int $idMember * @param int $idMember
@ -140,6 +182,24 @@ class TeamModel {
return $this->teams->getAll($user); return $this->teams->getAll($user);
} }
/**
* Get all user's teams where user is coach
* @param int $user
* @return array<array<string, mixed>>
*/
public function getAllIsCoach(int $user): array {
return $this->teams->getAllIsCoach($user);
}
/**
* Get all tactic shared with a team
* @param int $team
* @return array<array<string, mixed>>
*/
public function getAllTeamTactic(int $team): array {
return $this->teams->getAllTeamTactic($team);
}
/** /**
* @param int $start * @param int $start
* @param int $n * @param int $n

Loading…
Cancel
Save