-
-
Coach Sanae
+
+
+
+ {{
+ auth.user?.username ?? "Coach"
+ }}
+
+
diff --git a/src/components/AppSidebar.vue b/src/components/AppSidebar.vue
index eaa005f..6fe4b9a 100644
--- a/src/components/AppSidebar.vue
+++ b/src/components/AppSidebar.vue
@@ -6,6 +6,7 @@ import {
Activity,
GraduationCap,
History,
+ UserCog,
} from "lucide-vue-next";
const route = useRoute();
@@ -41,6 +42,12 @@ const navItems = [
icon: History,
label: "Historique Global",
},
+ {
+ to: "/users",
+ name: "users",
+ icon: UserCog,
+ label: "Utilisateurs",
+ },
];
const isActive = (name) => route.name === name;
diff --git a/src/composables/useAuthFetch.js b/src/composables/useAuthFetch.js
new file mode 100644
index 0000000..6783170
--- /dev/null
+++ b/src/composables/useAuthFetch.js
@@ -0,0 +1,15 @@
+import { useAuthStore } from "../stores/auth";
+
+export function useAuthFetch() {
+ function authFetch(url, options = {}) {
+ const auth = useAuthStore();
+ const headers = {
+ "Content-Type": "application/json",
+ ...(options.headers || {}),
+ };
+ if (auth.token) headers["Authorization"] = `Bearer ${auth.token}`;
+ return fetch(url, { ...options, headers });
+ }
+
+ return { authFetch };
+}
diff --git a/src/router/index.js b/src/router/index.js
index 24984e5..e6af1a7 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -4,16 +4,28 @@ import StudentsView from "../views/StudentsView.vue";
import ActivityView from "../views/ActivityView.vue";
import ResultsView from "../views/ResultsView.vue";
import HistoryView from "../views/HistoryView.vue";
+import LoginView from "../views/LoginView.vue";
+import UsersView from "../views/UsersView.vue";
const routes = [
+ { path: "/login", name: "login", component: LoginView, meta: { public: true } },
{ path: "/", name: "dashboard", component: DashboardView },
{ path: "/students", name: "students", component: StudentsView },
{ path: "/activity", name: "activity", component: ActivityView },
{ path: "/results", name: "results", component: ResultsView },
{ path: "/history", name: "history", component: HistoryView },
+ { path: "/users", name: "users", component: UsersView },
];
-export default createRouter({
+const router = createRouter({
history: createWebHashHistory(),
routes,
});
+
+router.beforeEach((to) => {
+ const token = localStorage.getItem("token");
+ if (!to.meta.public && !token) return { name: "login" };
+ if (to.name === "login" && token) return { name: "dashboard" };
+});
+
+export default router;
diff --git a/src/stores/auth.js b/src/stores/auth.js
new file mode 100644
index 0000000..3428f64
--- /dev/null
+++ b/src/stores/auth.js
@@ -0,0 +1,40 @@
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+
+export const useAuthStore = defineStore("auth", () => {
+ const token = ref(localStorage.getItem("token") || null);
+ const user = ref(JSON.parse(localStorage.getItem("user") || "null"));
+
+ const isAuthenticated = computed(() => !!token.value);
+
+ function setSession(data) {
+ token.value = data.token;
+ user.value = data.user;
+ localStorage.setItem("token", data.token);
+ localStorage.setItem("user", JSON.stringify(data.user));
+ }
+
+ function clearSession() {
+ token.value = null;
+ user.value = null;
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ }
+
+ async function login(username, password) {
+ const res = await fetch("/api/auth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username, password }),
+ });
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error || "Error al iniciar sesión");
+ setSession(data);
+ }
+
+ function logout() {
+ clearSession();
+ }
+
+ return { token, user, isAuthenticated, login, logout };
+});
diff --git a/src/stores/sports.js b/src/stores/sports.js
index 08d34a8..92242ee 100644
--- a/src/stores/sports.js
+++ b/src/stores/sports.js
@@ -1,186 +1,194 @@
-import { defineStore } from 'pinia'
-import { ref, computed } from 'vue'
-import { gradeWeights, calcIMC } from '../composables/useHelpers'
+import { defineStore } from "pinia";
+import { ref, computed } from "vue";
+import { gradeWeights, calcIMC } from "../composables/useHelpers";
+import { useAuthStore } from "./auth";
-const API = '/api'
+const API = "/api";
async function apiFetch(path, options = {}) {
- const res = await fetch(`${API}${path}`, {
- headers: { 'Content-Type': 'application/json' },
- ...options,
- })
- if (!res.ok) throw new Error(`API ${path} → ${res.status}`)
- return res.json()
+ const auth = useAuthStore();
+ const headers = { "Content-Type": "application/json" };
+ if (auth.token) headers["Authorization"] = `Bearer ${auth.token}`;
+ const res = await fetch(`${API}${path}`, { headers, ...options });
+ if (!res.ok) throw new Error(`API ${path} → ${res.status}`);
+ return res.json();
}
-export const useSportsStore = defineStore('sports', () => {
- const activities = ref([])
- const students = ref([])
- const notification = ref('')
- const loading = ref(false)
+export const useSportsStore = defineStore("sports", () => {
+ const activities = ref([]);
+ const students = ref([]);
+ const notification = ref("");
+ const loading = ref(false);
// ── Persistence ──────────────────────────────────────────────────────────
async function loadData() {
- loading.value = true
+ loading.value = true;
try {
const [s, a] = await Promise.all([
- apiFetch('/students'),
- apiFetch('/activities'),
- ])
- students.value = s
- activities.value = a
+ apiFetch("/students"),
+ apiFetch("/activities"),
+ ]);
+ students.value = s;
+ activities.value = a;
} catch (e) {
- showNotification('Erreur de connexion au serveur')
- console.error(e)
+ showNotification("Erreur de connexion au serveur");
+ console.error(e);
} finally {
- loading.value = false
+ loading.value = false;
}
}
// ── Notification ─────────────────────────────────────────────────────────
function showNotification(msg) {
- notification.value = msg
- setTimeout(() => (notification.value = ''), 3000)
+ notification.value = msg;
+ setTimeout(() => (notification.value = ""), 3000);
}
// ── Students ─────────────────────────────────────────────────────────────
async function saveStudent(form, editingId) {
- const imc = calcIMC(form.weight, form.height)
+ const imc = calcIMC(form.weight, form.height);
if (editingId) {
await apiFetch(`/students/${editingId}`, {
- method: 'PUT',
+ method: "PUT",
body: JSON.stringify({ ...form, imc }),
- })
- const idx = students.value.findIndex((s) => s.id === editingId)
- if (idx !== -1) students.value[idx] = { ...form, id: editingId, imc }
- showNotification('Profil mis à jour')
+ });
+ const idx = students.value.findIndex((s) => s.id === editingId);
+ if (idx !== -1) students.value[idx] = { ...form, id: editingId, imc };
+ showNotification("Profil mis à jour");
} else {
- const id = Date.now().toString()
- await apiFetch('/students', {
- method: 'POST',
+ const id = Date.now().toString();
+ await apiFetch("/students", {
+ method: "POST",
body: JSON.stringify({ id, ...form, imc }),
- })
- students.value.push({ id, ...form, imc })
- showNotification('Étudiant inscrit')
+ });
+ students.value.push({ id, ...form, imc });
+ showNotification("Étudiant inscrit");
}
}
async function deleteStudent(id) {
- await apiFetch(`/students/${id}`, { method: 'DELETE' })
- students.value = students.value.filter((s) => s.id !== id)
- activities.value = activities.value.filter((a) => a.studentId !== id)
- showNotification('Étudiant supprimé')
+ await apiFetch(`/students/${id}`, { method: "DELETE" });
+ students.value = students.value.filter((s) => s.id !== id);
+ activities.value = activities.value.filter((a) => a.studentId !== id);
+ showNotification("Étudiant supprimé");
}
// ── Activities ───────────────────────────────────────────────────────────
async function saveActivity(act, editingId) {
- let norm = act.durationInput
- if (act.durationUnit === 'sec') norm /= 60
- if (act.durationUnit === 'h') norm *= 60
+ let norm = act.durationInput;
+ if (act.durationUnit === "sec") norm /= 60;
+ if (act.durationUnit === "h") norm *= 60;
const record = {
...act,
id: editingId || Date.now().toString(),
duration: norm,
displayDuration: `${act.durationInput} ${act.durationUnit}`,
- }
+ };
if (editingId) {
await apiFetch(`/activities/${editingId}`, {
- method: 'PUT',
+ method: "PUT",
body: JSON.stringify(record),
- })
- const idx = activities.value.findIndex((a) => a.id === editingId)
- if (idx !== -1) activities.value[idx] = record
- showNotification('Activité mise à jour')
+ });
+ const idx = activities.value.findIndex((a) => a.id === editingId);
+ if (idx !== -1) activities.value[idx] = record;
+ showNotification("Activité mise à jour");
} else {
- await apiFetch('/activities', {
- method: 'POST',
+ await apiFetch("/activities", {
+ method: "POST",
body: JSON.stringify(record),
- })
- activities.value.unshift(record)
- showNotification('Activité enregistrée')
+ });
+ activities.value.unshift(record);
+ showNotification("Activité enregistrée");
}
}
async function deleteActivity(index) {
- const act = activities.value[index]
- await apiFetch(`/activities/${act.id}`, { method: 'DELETE' })
- activities.value.splice(index, 1)
- showNotification('Supprimé')
+ const act = activities.value[index];
+ await apiFetch(`/activities/${act.id}`, { method: "DELETE" });
+ activities.value.splice(index, 1);
+ showNotification("Supprimé");
}
// ── Helpers ──────────────────────────────────────────────────────────────
function getStudentName(id) {
- const s = students.value.find((st) => st.id === id)
- return s ? `${s.lastName} ${s.firstName}` : 'Inconnu'
+ const s = students.value.find((st) => st.id === id);
+ return s ? `${s.lastName} ${s.firstName}` : "Inconnu";
}
function getSelectedStudentIMC(id) {
- return students.value.find((s) => s.id === id)?.imc || 0
+ return students.value.find((s) => s.id === id)?.imc || 0;
}
function getStudentAveragePoints(studentId) {
- const evals = activities.value.filter((a) => a.studentId === studentId && a.grade)
- if (!evals.length) return -1
- return evals.reduce((acc, a) => acc + gradeWeights[a.grade], 0) / evals.length
+ const evals = activities.value.filter(
+ (a) => a.studentId === studentId && a.grade,
+ );
+ if (!evals.length) return -1;
+ return (
+ evals.reduce((acc, a) => acc + gradeWeights[a.grade], 0) / evals.length
+ );
}
function getStudentAverageLiteral(studentId) {
- const avg = getStudentAveragePoints(studentId)
- if (avg === -1) return 'Non noté'
- const rounded = Math.round(avg)
- return Object.keys(gradeWeights).find((k) => gradeWeights[k] === rounded)
+ const avg = getStudentAveragePoints(studentId);
+ if (avg === -1) return "Non noté";
+ const rounded = Math.round(avg);
+ return Object.keys(gradeWeights).find((k) => gradeWeights[k] === rounded);
}
function getStudentEvaluationsCount(id) {
- return activities.value.filter((a) => a.studentId === id && a.grade).length
+ return activities.value.filter((a) => a.studentId === id && a.grade).length;
}
// ── Computed stats ────────────────────────────────────────────────────────
const totalMinutes = computed(() =>
activities.value.reduce((acc, c) => acc + (c.duration || 0), 0),
- )
+ );
const successRate = computed(() =>
activities.value.length
? Math.round(
- (activities.value.filter((a) => a.grade && a.grade !== 'Redouble').length /
- activities.value.length) * 100,
+ (activities.value.filter((a) => a.grade && a.grade !== "Redouble")
+ .length /
+ activities.value.length) *
+ 100,
)
: 0,
- )
+ );
const gradeStats = computed(() => {
- const total = students.value.length || 1
- const averages = students.value.map((s) => getStudentAverageLiteral(s.id))
- const pct = (g) => Math.round((averages.filter((v) => v === g).length / total) * 100)
+ const total = students.value.length || 1;
+ const averages = students.value.map((s) => getStudentAverageLiteral(s.id));
+ const pct = (g) =>
+ Math.round((averages.filter((v) => v === g).length / total) * 100);
return {
- excellent: pct('Excellent'),
- tresBien: pct('Très bien'),
- bien: pct('Bien'),
- assezBien: pct('Assez bien'),
- redouble: pct('Redouble'),
- }
- })
+ excellent: pct("Excellent"),
+ tresBien: pct("Très bien"),
+ bien: pct("Bien"),
+ assezBien: pct("Assez bien"),
+ redouble: pct("Redouble"),
+ };
+ });
const activityLifestyleStats = computed(() => {
const stats = {
- Inactif: { count: 0, percentage: 0 },
- Sédentaire: { count: 0, percentage: 0 },
- Actif: { count: 0, percentage: 0 },
- 'Très actif': { count: 0, percentage: 0 },
- }
+ Inactif: { count: 0, percentage: 0 },
+ Sédentaire: { count: 0, percentage: 0 },
+ Actif: { count: 0, percentage: 0 },
+ "Très actif": { count: 0, percentage: 0 },
+ };
activities.value.forEach((a) => {
- const key = a.lifestyle || 'Sédentaire'
- if (stats[key]) stats[key].count++
- })
- const total = activities.value.length || 1
+ const key = a.lifestyle || "Sédentaire";
+ if (stats[key]) stats[key].count++;
+ });
+ const total = activities.value.length || 1;
Object.keys(stats).forEach(
(k) => (stats[k].percentage = Math.round((stats[k].count / total) * 100)),
- )
- return stats
- })
+ );
+ return stats;
+ });
return {
activities,
@@ -202,5 +210,5 @@ export const useSportsStore = defineStore('sports', () => {
successRate,
gradeStats,
activityLifestyleStats,
- }
-})
+ };
+});
diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue
new file mode 100644
index 0000000..e4b1136
--- /dev/null
+++ b/src/views/LoginView.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
SB SPORT
+
+ Sanae Benkhlifa
+
+
+
+
+
+
+
+
+ Accès sécurisé
+
+
+
+
diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue
new file mode 100644
index 0000000..3448d4d
--- /dev/null
+++ b/src/views/UsersView.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+
+
Utilisateurs
+
+
+
Chargement…
+
{{ error }}
+
+
+
+
+ | Utilisateur |
+ Rôle |
+ Créé le |
+ |
+
+
+
+
+ | {{ u.username }} |
+
+
+ {{ u.role }}
+
+ |
+ {{ u.createdAt?.slice(0, 10) }} |
+
+
+
+
+
+
+
+
+ {{ pwdError }}
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
Ajouter un utilisateur
+
+
+
+
+
+