qa: Managers can view team and manage theirs todo list

This commit is contained in:
nemunaire 2023-07-25 11:17:40 +02:00
parent b94beb363b
commit cd64fc90bf
13 changed files with 526 additions and 24 deletions

View File

@ -242,6 +242,17 @@ func (t *Team) NewQATodo(idExercice int64) (*QATodo, error) {
}
}
// Delete the comment in the database.
func (t *QATodo) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM teams_qa_todo WHERE id_todo = ?", t.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
// QAView
func (t *Team) GetQAView() (res []*QATodo, err error) {

View File

@ -29,4 +29,5 @@ func DeclareRoutes(router *gin.RouterGroup) {
}))
declareTodoManagerRoutes(apiManagerRoutes)
declareTeamsRoutes(apiManagerRoutes)
}

53
qa/api/team.go Normal file
View File

@ -0,0 +1,53 @@
package api
import (
"fmt"
"log"
"net/http"
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin"
)
func declareTeamsRoutes(router *gin.RouterGroup) {
router.GET("/teams", listTeams)
teamsRoutes := router.Group("/teams/:tid")
teamsRoutes.Use(teamHandler)
teamsRoutes.GET("", showTeam)
declareTodoRoutes(teamsRoutes)
declareTodoManagerRoutes(teamsRoutes)
}
func teamHandler(c *gin.Context) {
var team *fic.Team
if tid, err := strconv.ParseInt(string(c.Param("tid")), 10, 64); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad team identifier."})
return
} else if team, err = fic.GetTeam(tid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found."})
return
}
c.Set("team", team)
c.Next()
}
func listTeams(c *gin.Context) {
teams, err := fic.GetTeams()
if err != nil {
log.Println("Unable to GetTeams: ", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list teams: %s", err.Error())})
return
}
c.JSON(http.StatusOK, teams)
}
func showTeam(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("team"))
}

View File

@ -2,6 +2,7 @@ package api
import (
"net/http"
"strconv"
"srs.epita.fr/fic-server/libfic"
@ -18,12 +19,22 @@ func declareTodoRoutes(router *gin.RouterGroup) {
func declareTodoManagerRoutes(router *gin.RouterGroup) {
router.POST("/qa_my_exercices.json", addQAView)
router.POST("/qa_work.json", createQATodo)
todosRoutes := router.Group("/todo/:wid")
todosRoutes.Use(todoHandler)
todosRoutes.GET("", showTodo)
todosRoutes.DELETE("", deleteQATodo)
}
type exerciceTested map[int64]string
func getExerciceTested(c *gin.Context) {
teamid := c.MustGet("LoggedTeam").(int64)
var teamid int64
if team, ok := c.Get("team"); ok {
teamid = team.(*fic.Team).Id
} else {
teamid = c.MustGet("LoggedTeam").(int64)
}
team, err := fic.GetTeam(teamid)
if err != nil {
@ -55,7 +66,12 @@ func getExerciceTested(c *gin.Context) {
}
func getQAView(c *gin.Context) {
teamid := c.MustGet("LoggedTeam").(int64)
var teamid int64
if team, ok := c.Get("team"); ok {
teamid = team.(*fic.Team).Id
} else {
teamid = c.MustGet("LoggedTeam").(int64)
}
team, err := fic.GetTeam(teamid)
if err != nil {
@ -73,7 +89,12 @@ func getQAView(c *gin.Context) {
}
func getQAWork(c *gin.Context) {
teamid := c.MustGet("LoggedTeam").(int64)
var teamid int64
if team, ok := c.Get("team"); ok {
teamid = team.(*fic.Team).Id
} else {
teamid = c.MustGet("LoggedTeam").(int64)
}
team, err := fic.GetTeam(teamid)
if err != nil {
@ -91,7 +112,12 @@ func getQAWork(c *gin.Context) {
}
func getQATodo(c *gin.Context) {
teamid := c.MustGet("LoggedTeam").(int64)
var teamid int64
if team, ok := c.Get("team"); ok {
teamid = team.(*fic.Team).Id
} else {
teamid = c.MustGet("LoggedTeam").(int64)
}
team, err := fic.GetTeam(teamid)
if err != nil {
@ -167,3 +193,45 @@ func addQAView(c *gin.Context) {
c.JSON(http.StatusOK, view)
}
func todoHandler(c *gin.Context) {
team := c.MustGet("team").(*fic.Team)
var wid int64
var todos []*fic.QATodo
var err error
if wid, err = strconv.ParseInt(string(c.Param("wid")), 10, 64); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad todo identifier."})
return
}
if todos, err = team.GetQATodo(); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Todo not found."})
return
}
for _, t := range todos {
if t.Id == wid {
c.Set("todo", t)
c.Next()
return
}
}
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to find the requested QA Todo"})
}
func showTodo(c *gin.Context) {
c.JSON(http.StatusOK, c.MustGet("todo"))
}
func deleteQATodo(c *gin.Context) {
todo := c.MustGet("todo").(*fic.QATodo)
if _, err := todo.Delete(); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
} else {
c.Status(http.StatusOK)
}
}

View File

@ -121,6 +121,8 @@ func declareStaticRoutes(router *gin.RouterGroup, baseURL string) {
router.GET("/exercices", serveOrReverse("/", baseURL))
router.GET("/exercices/*_", serveOrReverse("/", baseURL))
router.GET("/export", serveOrReverse("/", baseURL))
router.GET("/teams", serveOrReverse("/", baseURL))
router.GET("/teams/*_", serveOrReverse("/", baseURL))
router.GET("/themes", serveOrReverse("/", baseURL))
router.GET("/themes/*_", serveOrReverse("/", baseURL))
router.GET("/_app/*_", serveOrReverse("", baseURL))

View File

@ -9,11 +9,12 @@
import { getExerciceQA } from '$lib/qa';
import { exercicesIdx } from '$lib/stores/exercices';
import { themesIdx } from '$lib/stores/themes';
import { todos } from '$lib/stores/todo';
export { className as class };
let className = '';
export let team = null;
function show(id) {
goto("exercices/" + id)
}
@ -23,7 +24,7 @@
setInterval(() => my_exercicesP = update_exercices(), 62000);
async function update_exercices() {
const view = await getQAView();
const view = await getQAView(team);
my_exercices = [];
for (const v of view) {
@ -50,7 +51,13 @@
>
</button>
<h3>Vos étapes</h3>
<h3>
{#if team}
Étapes de l'équipe
{:else}
Vos étapes
{/if}
</h3>
{#await my_exercicesP}
<div class="text-center">
<Spinner size="lg" />

View File

@ -13,10 +13,15 @@
export { className as class };
let className = '';
let exo_doneP = getExerciceTested();
export let team = null;
let teamtodos = todos;
$: teamtodos = team ? team.todos : todos;
let exo_doneP = getExerciceTested(team);
let tododone = { };
getQAWork().then((queries) => {
getQAWork(team).then((queries) => {
for (const q of queries) {
tododone[q.id_exercice] = q;
}
@ -30,12 +35,12 @@
<div class={className}>
<button
class="btn btn-dark float-end"
on:click|preventDefault={() => { todos.refresh(); exo_doneP = getExerciceTested(); }}
on:click|preventDefault={() => { todos.refresh(); exo_doneP = getExerciceTested(team); }}
>
</button>
<h3>Étapes à tester et valider</h3>
{#await todos.refresh()}
{#await teamtodos.refresh()}
<div class="text-center">
<Spinner size="lg" />
</div>
@ -54,7 +59,7 @@
</tr>
</thead>
<tbody>
{#each $todos as todo (todo.id)}
{#each $teamtodos as todo (todo.id)}
<tr
style:cursor="pointer"
class:text-light={!tododone[todo.id_exercice] && !exo_done[todo.id_exercice]}

View File

@ -0,0 +1,39 @@
import { writable, derived } from 'svelte/store';
import { getTeams } from '$lib/teams'
function createTeamsStore() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
set: (v) => {
update((m) => Object.assign(m, v));
},
update,
refresh: async () => {
const list = await getTeams();
update((m) => list);
return list;
},
};
}
export const teams = createTeamsStore();
export const teamsIdx = derived(
teams,
$teams => {
const teams_idx = { };
for (const e of $teams) {
teams_idx[e.id] = e;
}
return teams_idx;
},
);

View File

@ -2,7 +2,7 @@ import { writable, derived } from 'svelte/store';
import { getQAView, getQATodo, getQAWork } from '$lib/todo';
function createTodosStore() {
export function createTodosStore(team) {
const { subscribe, set, update } = writable([]);
return {
@ -15,8 +15,8 @@ function createTodosStore() {
update,
refresh: async () => {
const list = await getQATodo();
list.push(...await getQAWork());
const list = await getQATodo(team);
list.push(...await getQAWork(team));
update((m) => list);
return list;
},

49
qa/ui/src/lib/teams.js Normal file
View File

@ -0,0 +1,49 @@
import { createTodosStore } from '$lib/stores/todo.js'
export const fieldsTeams = ["name", "color", "active", "external_id"];
export class Team {
constructor(res) {
if (res) {
this.update(res);
}
this.todos = createTodosStore(this);
}
update({ id, name, color, active, external_id }) {
this.id = id;
this.name = name;
this.color = color;
this.active = active;
this.external_id = external_id;
}
toHexColor() {
let num = this.color;
num >>>= 0;
let b = num & 0xFF,
g = (num & 0xFF00) >>> 8,
r = (num & 0xFF0000) >>> 16,
a = ( (num & 0xFF000000) >>> 24 ) / 255 ;
return "#" + r.toString(16) + g.toString(16) + b.toString(16);
}
}
export async function getTeams() {
const res = await fetch(`api/teams`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((t) => new Team(t));
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getTeam(tid) {
const res = await fetch(`api/teams/${tid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return new Team(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}

View File

@ -12,19 +12,36 @@ export class QATodo {
this.id_team = id_team;
this.id_exercice = id_exercice;
}
async delete(team) {
const res = await fetch(team?`api/teams/${team.id}/todo/${this.id}`:`api/teams/${this.id_team}/todo/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'}
});
if (res.status < 300) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
}
export async function getQATodo() {
const res = await fetch(`api/qa_work.json`, {headers: {'Accept': 'application/json'}})
export async function getQATodo(team) {
const res = await fetch(team?`api/teams/${team.id}/qa_work.json`:`api/qa_work.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((t) => new QATodo(t));
const data = await res.json();
if (data === null) {
return []
} else {
return data.map((t) => new QATodo(t));
}
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getQAWork() {
const res = await fetch(`api/qa_mywork.json`, {headers: {'Accept': 'application/json'}})
export async function getQAWork(team) {
const res = await fetch(team?`api/teams/${team.id}/qa_mywork.json`:`api/qa_mywork.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const data = await res.json()
if (data) {
@ -37,8 +54,8 @@ export async function getQAWork() {
}
}
export async function getQAView() {
const res = await fetch(`api/qa_myexercices.json`, {headers: {'Accept': 'application/json'}})
export async function getQAView(team) {
const res = await fetch(team?`api/teams/${team.id}/qa_myexercices.json`:`api/qa_myexercices.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const data = await res.json()
if (data) {
@ -51,8 +68,8 @@ export async function getQAView() {
}
}
export async function getExerciceTested() {
const res = await fetch(`api/qa_exercices.json`, {headers: {'Accept': 'application/json'}})
export async function getExerciceTested(team) {
const res = await fetch(team?`api/teams/${team.id}/qa_exercices.json`:`api/qa_exercices.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {

View File

@ -0,0 +1,53 @@
<script>
import { goto } from '$app/navigation';
import { teams } from '$lib/stores/teams';
import {
Container,
Table,
} from 'sveltestrap';
teams.refresh();
let query = "";
const fields = ["name", "color", "active", "external_id"];
function show(id) {
goto("teams/" + id)
}
</script>
<Container class="mt-2 mb-5">
<h2>
Équipes
</h2>
<p>
<input type="search" class="form-control" placeholder="Filtrer" bind:value={query} autofocus>
</p>
<Table class="table-hover table-bordered table-striped">
<thead class="thead-dark">
<tr>
{#each fields as field}
<th>
{field}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each $teams as team (team.id)}
{#if team.name.indexOf(query) >= 0}
<tr on:click={() => show(team.id)}>
{#each fields as field}
<td class:text-end={field == "image"}>
{team[field]}
</td>
{/each}
</tr>
{/if}
{/each}
</tbody>
</Table>
</Container>

View File

@ -0,0 +1,197 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
Button,
Col,
Container,
Row,
Spinner,
} from 'sveltestrap';
import MyExercices from '$lib/components/MyExercices.svelte';
import MyTodo from '$lib/components/MyTodo.svelte';
import { exercices, exercicesByTheme, exercicesIdx } from '$lib/stores/exercices'
import { themes, themesIdx } from '$lib/stores/themes'
import { getTeam } from '$lib/teams';
exercices.refresh();
themes.refresh();
let teamtodos = null;
let teamP = getTeam($page.params.tid);
let todosP = null;
teamP.then((team) => {
teamtodos = team.todos;
todosP = team.todos.refresh();
});
let newTodo = 0;
async function submitNewTodo(team) {
const res = await fetch(`api/qa_work.json`, {
method: 'POST',
headers: {'Accept': 'application/json'},
body: JSON.stringify({
id_team: team.id,
id_exercice: newTodo,
}),
})
if (res.status == 200) {
newTodo = 0;
todosP = team.todos.refresh();
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
let newThemeTodo = 0;
async function submitNewThemeTodo(team) {
for(const e of $exercicesByTheme[newThemeTodo]) {
await fetch(`api/qa_work.json`, {
method: 'POST',
headers: {'Accept': 'application/json'},
body: JSON.stringify({
id_team: team.id,
id_exercice: e.id,
}),
})
}
newThemeTodo = 0;
todosP = team.todos.refresh();
}
</script>
{#await teamP}
<Container class="mt-2 mb-5">
<div class="d-flex justify-content-center">
<Spinner size="lg" />
</div>
</Container>
{:then team}
<Container class="mt-2 mb-5">
<h2 class="d-flex align-items-center">
<div
style={"width: 1em; height: 1em; background-color: " + team.toHexColor()}
class="me-2 rounded"
/>
{team.name}
</h2>
<Row>
<Col>
<h3>Tâches de l'équipe</h3>
{#await todosP}
<div class="text-center">
<Spinner size="lg" />
</div>
{:then}
<table class="table table-stripped table-hover">
<thead>
<tr>
<th>Scénario</th>
<th>Défi</th>
<th>Act</th>
</tr>
</thead>
<tbody>
{#each $teamtodos as todo (todo.id)}
<tr>
<td>
{#if $exercicesIdx.length == 0 && $themesIdx.length == 0}
<Spinner size="sm" />
{:else}
<a href="themes/{$exercicesIdx[todo.id_exercice].id_theme}">
{$themesIdx[$exercicesIdx[todo.id_exercice].id_theme].name}
</a>
{/if}
</td>
<td>
{#if $exercicesIdx.length == 0}
<Spinner size="sm" />
{:else}
{$exercicesIdx[todo.id_exercice].title}
{/if}
</td>
<td class="text-end">
<Button
color="danger"
size="sm"
on:click={todo.delete(team).then(() => { todosP = team.todos.refresh(); })}
>
🗑
</Button>
</td>
</tr>
{/each}
</tbody>
<tfoot>
<tr>
<td colspan="3">
<form
on:submit={() => submitNewTodo(team)}
>
<div class="input-group">
<label class="input-group-text" for="exerciceTodo">Exercice</label>
<select
class="form-select"
id="exercice"
bind:value={newTodo}
>
{#each Object.keys($exercicesByTheme) as thid}
<optgroup label={$themesIdx[thid].name}>
{#each $exercicesByTheme[thid] as exercice (exercice.id)}
<option value={exercice.id}>{exercice.title}</option>
{/each}
</optgroup>
{/each}
</select>
<Button
color="success"
type="submit"
>
+
</Button>
</div>
</form>
</td>
</tr>
<tr>
<td colspan="3">
<form
on:submit={() => submitNewThemeTodo(team)}
>
<div class="input-group">
<label class="input-group-text" for="themeTodo">Thème</label>
<select
class="form-select"
id="themeTodo"
bind:value={newThemeTodo}
>
{#each Object.keys($exercicesByTheme) as thid}
<option value={$themesIdx[thid].id}>{$themesIdx[thid].name}</option>
{/each}
</select>
<Button
color="success"
type="submit"
>
+
</Button>
</div>
</form>
</td>
</tr>
</tfoot>
</table>
{/await}
</Col>
<Col>
<MyExercices {team} />
<MyTodo {team} />
</Col>
</Row>
</Container>
{/await}