diff --git a/backend/src/controllers/friend.controller.ts b/backend/src/controllers/friend.controller.ts index e6b426b..5dc2094 100644 --- a/backend/src/controllers/friend.controller.ts +++ b/backend/src/controllers/friend.controller.ts @@ -2,6 +2,7 @@ import type { Response } from "express"; import { prisma } from "../index"; import type { AppRequest } from "../types/context"; import { friendRequestSchema, friendResponseSchema } from "../validators/friend.validators"; +import { checkAndGrantRewards } from "../services/reward.service"; // Envoyer une demande d'ami export async function sendRequest(req: AppRequest, res: Response): Promise { @@ -71,6 +72,15 @@ export async function respondToRequest(req: AppRequest, res: Response): Promise< sender: { select: { id: true, username: true, avatar: true } }, }, }); + + // Déclenche les récompenses pour les deux parties si accepté + if (parsed.data.status === "ACCEPTED") { + await Promise.all([ + checkAndGrantRewards(req.user!.id), + checkAndGrantRewards(updated.sender.id), + ]); + } + res.json(updated); } diff --git a/backend/src/controllers/history.controller.ts b/backend/src/controllers/history.controller.ts index e877d37..3583eb5 100644 --- a/backend/src/controllers/history.controller.ts +++ b/backend/src/controllers/history.controller.ts @@ -6,6 +6,7 @@ import { completeHistorySchema, startHistorySchema, } from "../validators/history.validators"; +import { checkAndGrantRewards } from "../services/reward.service"; // Démarre une session d'entraînement export async function start(req: AppRequest, res: Response): Promise { @@ -91,7 +92,8 @@ export async function complete(req: AppRequest, res: Response): Promise { }, }); - res.json(completed); + const newRewards = await checkAndGrantRewards(req.user!.id); + res.json({ ...completed, newRewards }); } // Historique de l'utilisateur connecté diff --git a/backend/src/controllers/reward.controller.ts b/backend/src/controllers/reward.controller.ts new file mode 100644 index 0000000..86ed56a --- /dev/null +++ b/backend/src/controllers/reward.controller.ts @@ -0,0 +1,19 @@ +import type { Response } from "express"; +import { prisma } from "../index"; +import type { AppRequest } from "../types/context"; + +// Liste toutes les récompenses disponibles +export async function getAll(_req: AppRequest, res: Response): Promise { + const rewards = await prisma.reward.findMany({ orderBy: { name: "asc" } }); + res.json(rewards); +} + +// Récompenses débloquées par l'utilisateur connecté +export async function getMyRewards(req: AppRequest, res: Response): Promise { + const rewards = await prisma.userReward.findMany({ + where: { userId: req.user!.id }, + include: { reward: true }, + orderBy: { earnedAt: "desc" }, + }); + res.json(rewards); +} diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 004b636..00e3d11 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -98,6 +98,20 @@ async function main() { }); console.log("✅ Programme : Cardio Intermédiaire"); + // ── Récompenses ─────────────────────────────────────────────────────────── + const rewardsData = [ + { name: "Premier pas", description: "Terminer son premier programme.", condition: "first_program" }, + { name: "Régulier", description: "Terminer 5 programmes.", condition: "five_programs" }, + { name: "Acharné", description: "Terminer 10 programmes.", condition: "ten_programs" }, + { name: "Premier ami", description: "Avoir son premier ami.", condition: "first_friend" }, + { name: "Populaire", description: "Avoir 5 amis.", condition: "five_friends" }, + { name: "Esprit d'équipe", description: "Rejoindre un groupe.", condition: "first_group" }, + { name: "Créateur", description: "Créer son premier programme.", condition: "first_program_created" }, + ]; + + await prisma.reward.createMany({ data: rewardsData, skipDuplicates: true }); + console.log(`✅ ${rewardsData.length} récompenses`); + console.log("🎉 Seed terminé."); } diff --git a/backend/src/index.ts b/backend/src/index.ts index 7747036..e06cadd 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -11,6 +11,7 @@ import programRoutes from "./routes/program.routes"; import historyRoutes from "./routes/history.routes"; import friendRoutes from "./routes/friend.routes"; import groupRoutes from "./routes/group.routes"; +import rewardRoutes from "./routes/reward.routes"; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const app = express(); @@ -42,6 +43,7 @@ app.use("/api/programs", programRoutes); app.use("/api/history", historyRoutes); app.use("/api/friends", friendRoutes); app.use("/api/groups", groupRoutes); +app.use("/api/rewards", rewardRoutes); app.get("/health", (_req, res) => { res.json({ status: "ok" }); diff --git a/backend/src/routes/reward.routes.ts b/backend/src/routes/reward.routes.ts new file mode 100644 index 0000000..4ceae55 --- /dev/null +++ b/backend/src/routes/reward.routes.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { getAll, getMyRewards } from "../controllers/reward.controller"; +import { requireAuth } from "../middlewares/auth"; + +const router = Router(); + +router.get("/", requireAuth, getAll); +router.get("/me", requireAuth, getMyRewards); + +export default router; diff --git a/backend/src/services/reward.service.ts b/backend/src/services/reward.service.ts new file mode 100644 index 0000000..3a3229d --- /dev/null +++ b/backend/src/services/reward.service.ts @@ -0,0 +1,76 @@ +import { prisma } from "../index"; + +// Conditions disponibles — à étendre librement +const CONDITIONS: Record Promise> = { + first_program: async (userId) => { + const count = await prisma.history.count({ + where: { userId, completedAt: { not: null } }, + }); + return count >= 1; + }, + five_programs: async (userId) => { + const count = await prisma.history.count({ + where: { userId, completedAt: { not: null } }, + }); + return count >= 5; + }, + ten_programs: async (userId) => { + const count = await prisma.history.count({ + where: { userId, completedAt: { not: null } }, + }); + return count >= 10; + }, + first_friend: async (userId) => { + const count = await prisma.friendRequest.count({ + where: { + status: "ACCEPTED", + OR: [{ senderId: userId }, { receiverId: userId }], + }, + }); + return count >= 1; + }, + five_friends: async (userId) => { + const count = await prisma.friendRequest.count({ + where: { + status: "ACCEPTED", + OR: [{ senderId: userId }, { receiverId: userId }], + }, + }); + return count >= 5; + }, + first_group: async (userId) => { + const count = await prisma.groupMember.count({ where: { userId } }); + return count >= 1; + }, + first_program_created: async (userId) => { + const count = await prisma.program.count({ where: { authorId: userId } }); + return count >= 1; + }, +}; + +// Vérifie et attribue toutes les récompenses débloquées pour un utilisateur +export async function checkAndGrantRewards(userId: string): Promise { + const allRewards = await prisma.reward.findMany(); + const earned = await prisma.userReward.findMany({ + where: { userId }, + select: { rewardId: true }, + }); + const earnedIds = new Set(earned.map((r) => r.rewardId)); + + const newlyEarned: string[] = []; + + for (const reward of allRewards) { + if (earnedIds.has(reward.id!)) continue; + + const checker = CONDITIONS[reward.condition]; + if (!checker) continue; + + const unlocked = await checker(userId); + if (unlocked) { + await prisma.userReward.create({ data: { userId, rewardId: reward.id! } }); + newlyEarned.push(reward.name); + } + } + + return newlyEarned; +}