Skip to content

Commit 897f99d

Browse files
committed
feat(dashboard): Fixes #138 - Dashboard membres et teams
1 parent 5f7bd3e commit 897f99d

8 files changed

Lines changed: 212 additions & 47 deletions

File tree

.github/workflows/ticket.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ jobs:
2424
exit 0
2525
fi
2626
27-
# Regex pour le commit / titre PR : type(nom): Fixes #<num> - message
28-
if [[ "$PR_TITLE" =~ ^(feat|feature|fix|docs|chore|refactor|test|hotfix)\([a-zA-Z0-9_-]+\):\ Fixes\ \#[0-9]+\ -\ .+ ]]; then
27+
# Regex pour le titre PR : type(nom): Fixes #<num> - message
28+
TITLE_REGEX='^(feat|feature|fix|docs|chore|refactor|test|hotfix)\([a-zA-Z0-9_-]+\): Fixes #[0-9]+ - .+'
29+
if [[ "$PR_TITLE" =~ $TITLE_REGEX ]]; then
2930
echo "✅ Titre de PR valide avec référence au ticket"
3031
exit 0
3132
fi
3233
33-
# Regex pour le nom de branche : feature/123-description, fix/123-description, hotfix/123-description
34-
if [[ "$BRANCH_NAME" =~ ^(feature|fix|hotfix)/[0-9]+-.+ ]]; then
34+
# Regex pour le nom de branche : feat/123-description, feature/123-description, fix/123-description, hotfix/123-description
35+
if [[ "$BRANCH_NAME" =~ ^(feat|feature|fix|hotfix)/[0-9]+-.+ ]]; then
3536
echo "✅ Nom de branche valide avec référence au ticket"
3637
exit 0
3738
fi

saintBarthVolleyApp/frontend/src/app/admin/page.tsx

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ interface Stats {
1919
activePartners: number;
2020
}
2121

22+
interface ScrapingResult {
23+
matchesCreated: number;
24+
matchesUpdated: number;
25+
standings: number;
26+
errors: string[];
27+
logs: string[];
28+
}
29+
2230
interface QuickLink {
2331
label: string;
2432
href: string;
@@ -46,11 +54,6 @@ const QUICK_LINKS: QuickLink[] = [
4654
href: "/admin/partners",
4755
description: "Gérer les sponsors et partenaires",
4856
},
49-
{
50-
label: "Matches & scraping",
51-
href: "/admin/matches",
52-
description: "Voir les matches et lancer le scraping FFVB",
53-
},
5457
{
5558
label: "Infos du club",
5659
href: "/admin/club",
@@ -62,27 +65,101 @@ export default function AdminDashboardPage() {
6265
const [stats, setStats] = React.useState<Stats | null>(null);
6366
const [loading, setLoading] = React.useState(true);
6467

68+
const [scraping, setScraping] = React.useState(false);
69+
const [scrapingResult, setScrapingResult] =
70+
React.useState<ScrapingResult | null>(null);
71+
const [scrapingError, setScrapingError] = React.useState<string | null>(null);
72+
const [showLogs, setShowLogs] = React.useState(false);
73+
6574
React.useEffect(() => {
66-
apiFetch("/api/stats")
75+
apiFetch<Stats>("/api/stats")
6776
.then(setStats)
6877
.catch(console.error)
6978
.finally(() => setLoading(false));
7079
}, []);
7180

81+
const handleScraping = async () => {
82+
if (
83+
!confirm("Lancer le scraping FFVB ? Cela peut prendre plusieurs minutes.")
84+
)
85+
return;
86+
setScraping(true);
87+
setScrapingResult(null);
88+
setScrapingError(null);
89+
try {
90+
const result = await apiFetch<ScrapingResult>("/api/scraping/run", {
91+
method: "POST",
92+
});
93+
setScrapingResult(result);
94+
// Refresh stats after scraping
95+
const updated = await apiFetch<Stats>("/api/stats").catch(() => null);
96+
if (updated) setStats(updated);
97+
} catch (err) {
98+
setScrapingError(
99+
err instanceof Error ? err.message : "Erreur lors du scraping",
100+
);
101+
} finally {
102+
setScraping(false);
103+
}
104+
};
105+
72106
return (
73107
<div className="flex flex-1 flex-col gap-8">
74-
<div>
75-
<h1 className="text-2xl font-bold">Dashboard</h1>
76-
{stats?.activeSeason && (
77-
<p className="text-sm text-muted-foreground mt-1">
78-
Saison active :{" "}
79-
<span className="font-medium text-foreground">
80-
{stats.activeSeason}
81-
</span>
82-
</p>
83-
)}
108+
<div className="flex items-start justify-between gap-4 flex-wrap">
109+
<div>
110+
<h1 className="text-2xl font-bold">Dashboard</h1>
111+
{stats?.activeSeason && (
112+
<p className="text-sm text-muted-foreground mt-1">
113+
Saison active :{" "}
114+
<span className="font-medium text-foreground">
115+
{stats.activeSeason}
116+
</span>
117+
</p>
118+
)}
119+
</div>
120+
121+
<Button
122+
onClick={handleScraping}
123+
disabled={scraping}
124+
className="bg-orange-600 hover:bg-orange-700 text-white shrink-0"
125+
>
126+
{scraping ? "⟳ Scraping en cours..." : "Lancer le scraping FFVB"}
127+
</Button>
84128
</div>
85129

130+
{/* Résultat scraping */}
131+
{scrapingResult && (
132+
<div className="border rounded-lg p-4 bg-green-50 border-green-200 flex flex-col gap-2">
133+
<div className="font-semibold text-green-800">Scraping terminé</div>
134+
<div className="text-sm text-green-700 flex flex-wrap gap-4">
135+
<span>{scrapingResult.matchesCreated} match(es) créé(s)</span>
136+
<span>{scrapingResult.matchesUpdated} mis à jour</span>
137+
<span>{scrapingResult.standings} classement(s)</span>
138+
</div>
139+
{scrapingResult.errors.filter(Boolean).length > 0 && (
140+
<div className="text-sm text-red-600">
141+
Erreurs : {scrapingResult.errors.join(", ")}
142+
</div>
143+
)}
144+
<button
145+
className="text-xs text-green-600 underline self-start"
146+
onClick={() => setShowLogs((v) => !v)}
147+
>
148+
{showLogs ? "Masquer les logs" : "Voir les logs"}
149+
</button>
150+
{showLogs && (
151+
<pre className="text-xs bg-white border rounded p-3 overflow-auto max-h-48 whitespace-pre-wrap">
152+
{scrapingResult.logs.join("\n")}
153+
</pre>
154+
)}
155+
</div>
156+
)}
157+
{scrapingError && (
158+
<div className="border rounded-lg p-4 bg-red-50 border-red-200 text-red-700 text-sm">
159+
{scrapingError}
160+
</div>
161+
)}
162+
86163
<SectionAdminCards stats={stats} loading={loading} />
87164

88165
{/* Accès rapides */}

saintBarthVolleyApp/frontend/src/app/admin/seasons/[seasonId]/teams/[teamId]/page.tsx

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ interface Standing {
4848
setsAgainst: number;
4949
}
5050

51+
interface Match {
52+
_id: string;
53+
opponentName: string;
54+
date: string;
55+
homeAway: "home" | "away";
56+
status: "scheduled" | "played";
57+
scoreFor?: number;
58+
scoreAgainst?: number;
59+
}
60+
5161
interface TeamRole {
5262
_id: string; // subdoc ObjectId
5363
teamId: string;
@@ -123,6 +133,7 @@ export default function TeamDetailPage() {
123133
const [team, setTeam] = React.useState<Team | null>(null);
124134
const [form, setForm] = React.useState<Team | null>(null);
125135
const [standings, setStandings] = React.useState<Standing[]>([]);
136+
const [matches, setMatches] = React.useState<Match[]>([]);
126137
const [allClubMembers, setAllClubMembers] = React.useState<Member[]>([]);
127138
const [loading, setLoading] = React.useState(true);
128139
const [saving, setSaving] = React.useState(false);
@@ -167,25 +178,27 @@ export default function TeamDetailPage() {
167178

168179
// ── Load ──────────────────────────────────────────────────────────────────
169180
const refreshMembers = React.useCallback(async () => {
170-
const data: Member[] = await apiFetch("/api/members");
181+
const data = await apiFetch<Member[]>("/api/members");
171182
setAllClubMembers(data);
172183
}, []);
173184

174185
React.useEffect(() => {
175186
if (!id) return;
176187
Promise.all([
177-
apiFetch(`/api/teams/${id}`),
178-
apiFetch(`/api/standings?teamId=${id}`),
179-
apiFetch("/api/members"),
188+
apiFetch<Team>(`/api/teams/${id}`),
189+
apiFetch<Standing[]>(`/api/standings?teamId=${id}`),
190+
apiFetch<Member[]>("/api/members"),
191+
apiFetch<Match[]>(`/api/matches?teamId=${id}`),
180192
])
181-
.then(([teamData, standingsData, membersData]) => {
193+
.then(([teamData, standingsData, membersData, matchesData]) => {
182194
setTeam(teamData);
183195
setForm({
184196
...teamData,
185197
trainingSchedule: teamData.trainingSchedule ?? [],
186198
});
187199
setStandings(standingsData);
188-
setAllClubMembers(membersData as Member[]);
200+
setAllClubMembers(membersData);
201+
setMatches(matchesData);
189202
})
190203
.catch(() => alert("Erreur lors du chargement"))
191204
.finally(() => setLoading(false));
@@ -196,7 +209,7 @@ export default function TeamDetailPage() {
196209
if (!form) return;
197210
setSaving(true);
198211
try {
199-
const updated = await apiFetch(`/api/teams/${form._id}`, {
212+
const updated = await apiFetch<Team>(`/api/teams/${form._id}`, {
200213
method: "PUT",
201214
body: JSON.stringify(form),
202215
});
@@ -482,6 +495,79 @@ export default function TeamDetailPage() {
482495
</section>
483496
)}
484497

498+
{/* ── Matches ── */}
499+
{matches.length > 0 && (
500+
<section className="border rounded-lg p-6 flex flex-col gap-4">
501+
<h2 className="text-lg font-semibold">
502+
Matches
503+
<span className="ml-2 text-sm font-normal text-muted-foreground">
504+
({matches.length})
505+
</span>
506+
</h2>
507+
<div className="overflow-auto rounded border">
508+
<table className="w-full text-sm">
509+
<thead className="bg-muted">
510+
<tr>
511+
<th className="p-2 text-left">Date</th>
512+
<th className="p-2 text-left">Adversaire</th>
513+
<th className="p-2 text-center">D/E</th>
514+
<th className="p-2 text-center">Statut</th>
515+
<th className="p-2 text-center">Score</th>
516+
</tr>
517+
</thead>
518+
<tbody>
519+
{[...matches]
520+
.sort(
521+
(a, b) =>
522+
new Date(a.date).getTime() - new Date(b.date).getTime(),
523+
)
524+
.map((m) => (
525+
<tr key={m._id} className="border-t">
526+
<td className="p-2 whitespace-nowrap text-muted-foreground">
527+
{new Date(m.date).toLocaleString("fr-FR", {
528+
day: "2-digit",
529+
month: "2-digit",
530+
year: "numeric",
531+
hour: "2-digit",
532+
minute: "2-digit",
533+
})}
534+
</td>
535+
<td className="p-2 font-medium">{m.opponentName}</td>
536+
<td className="p-2 text-center">
537+
<span
538+
className={`px-2 py-0.5 rounded text-xs font-medium ${
539+
m.homeAway === "home"
540+
? "bg-blue-100 text-blue-700"
541+
: "bg-purple-100 text-purple-700"
542+
}`}
543+
>
544+
{m.homeAway === "home" ? "Dom." : "Ext."}
545+
</span>
546+
</td>
547+
<td className="p-2 text-center">
548+
<span
549+
className={`px-2 py-0.5 rounded text-xs font-medium ${
550+
m.status === "played"
551+
? "bg-green-100 text-green-700"
552+
: "bg-yellow-100 text-yellow-700"
553+
}`}
554+
>
555+
{m.status === "played" ? "Joué" : "Prévu"}
556+
</span>
557+
</td>
558+
<td className="p-2 text-center font-mono">
559+
{m.status === "played"
560+
? `${m.scoreFor} - ${m.scoreAgainst}`
561+
: "-"}
562+
</td>
563+
</tr>
564+
))}
565+
</tbody>
566+
</table>
567+
</div>
568+
</section>
569+
)}
570+
485571
{/* ── Joueurs & Staff ── */}
486572
<section className="border rounded-lg p-6 flex flex-col gap-4">
487573
<div className="flex items-center justify-between">

saintBarthVolleyApp/frontend/src/app/admin/seasons/[seasonId]/teams/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function SeasonTeamsPage() {
2828
if (!seasonId || Array.isArray(seasonId)) return;
2929
setLoading(true);
3030
try {
31-
const data: Team[] = await apiFetch(`/api/teams?seasonId=${seasonId}`);
31+
const data = await apiFetch<Team[]>(`/api/teams?seasonId=${seasonId}`);
3232
setTeams(data);
3333
} catch (err) {
3434
console.error(err);
@@ -60,7 +60,7 @@ export default function SeasonTeamsPage() {
6060
try {
6161
if (team._id) {
6262
// Mise à jour
63-
const updated = await apiFetch(`/api/teams/${team._id}`, {
63+
const updated = await apiFetch<Team>(`/api/teams/${team._id}`, {
6464
method: "PUT",
6565
body: JSON.stringify(team),
6666
});
@@ -69,7 +69,7 @@ export default function SeasonTeamsPage() {
6969
);
7070
} else {
7171
// Création
72-
const created = await apiFetch(`/api/teams`, {
72+
const created = await apiFetch<Team>(`/api/teams`, {
7373
method: "POST",
7474
body: JSON.stringify(team),
7575
});

saintBarthVolleyApp/frontend/src/app/verify-email/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { useEffect, useState } from "react";
66
import { useSearchParams, useRouter } from "next/navigation";
77
import { apiFetch } from "@/lib/api";
8+
import type { ApiMessage } from "@/lib/auth";
89
import {
910
Card,
1011
CardContent,
@@ -34,7 +35,9 @@ export default function VerifyEmailPage() {
3435

3536
const verify = async () => {
3637
try {
37-
const res = await apiFetch(`/api/auth/verify-email?token=${token}`);
38+
const res = await apiFetch<ApiMessage>(
39+
`/api/auth/verify-email?token=${token}`,
40+
);
3841
setStatus("success");
3942
setMessage(res.message);
4043
} catch (err: any) {

saintBarthVolleyApp/frontend/src/components/auth/register-form.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "@/components/ui/card";
1515
import Link from "next/link";
1616
import { apiFetch } from "@/lib/api";
17+
import type { ApiMessage } from "@/lib/auth";
1718

1819
export function RegisterForm() {
1920
const [form, setForm] = useState({
@@ -56,7 +57,7 @@ export function RegisterForm() {
5657
}
5758

5859
try {
59-
const data = await apiFetch("/api/auth/register", {
60+
const data = await apiFetch<ApiMessage>("/api/auth/register", {
6061
method: "POST",
6162
body: JSON.stringify({
6263
firstName: form.firstName,

0 commit comments

Comments
 (0)