Compare commits
5 Commits
feat/phase
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6841571eed | |||
| e57e13781a | |||
| 45c3bb8e8b | |||
| 6b08ebf779 | |||
| 566daedd01 |
@@ -2,6 +2,7 @@ import type { Response } from "express";
|
|||||||
import { prisma } from "../index";
|
import { prisma } from "../index";
|
||||||
import type { AppRequest } from "../types/context";
|
import type { AppRequest } from "../types/context";
|
||||||
import { friendRequestSchema, friendResponseSchema } from "../validators/friend.validators";
|
import { friendRequestSchema, friendResponseSchema } from "../validators/friend.validators";
|
||||||
|
import { checkAndGrantRewards } from "../services/reward.service";
|
||||||
|
|
||||||
// Envoyer une demande d'ami
|
// Envoyer une demande d'ami
|
||||||
export async function sendRequest(req: AppRequest, res: Response): Promise<void> {
|
export async function sendRequest(req: AppRequest, res: Response): Promise<void> {
|
||||||
@@ -71,6 +72,15 @@ export async function respondToRequest(req: AppRequest, res: Response): Promise<
|
|||||||
sender: { select: { id: true, username: true, avatar: true } },
|
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);
|
res.json(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
completeHistorySchema,
|
completeHistorySchema,
|
||||||
startHistorySchema,
|
startHistorySchema,
|
||||||
} from "../validators/history.validators";
|
} from "../validators/history.validators";
|
||||||
|
import { checkAndGrantRewards } from "../services/reward.service";
|
||||||
|
|
||||||
// Démarre une session d'entraînement
|
// Démarre une session d'entraînement
|
||||||
export async function start(req: AppRequest, res: Response): Promise<void> {
|
export async function start(req: AppRequest, res: Response): Promise<void> {
|
||||||
@@ -91,7 +92,8 @@ export async function complete(req: AppRequest, res: Response): Promise<void> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(completed);
|
const newRewards = await checkAndGrantRewards(req.user!.id);
|
||||||
|
res.json({ ...completed, newRewards });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Historique de l'utilisateur connecté
|
// Historique de l'utilisateur connecté
|
||||||
|
|||||||
19
backend/src/controllers/reward.controller.ts
Normal file
19
backend/src/controllers/reward.controller.ts
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
const rewards = await prisma.userReward.findMany({
|
||||||
|
where: { userId: req.user!.id },
|
||||||
|
include: { reward: true },
|
||||||
|
orderBy: { earnedAt: "desc" },
|
||||||
|
});
|
||||||
|
res.json(rewards);
|
||||||
|
}
|
||||||
@@ -98,6 +98,20 @@ async function main() {
|
|||||||
});
|
});
|
||||||
console.log("✅ Programme : Cardio Intermédiaire");
|
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é.");
|
console.log("🎉 Seed terminé.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import programRoutes from "./routes/program.routes";
|
|||||||
import historyRoutes from "./routes/history.routes";
|
import historyRoutes from "./routes/history.routes";
|
||||||
import friendRoutes from "./routes/friend.routes";
|
import friendRoutes from "./routes/friend.routes";
|
||||||
import groupRoutes from "./routes/group.routes";
|
import groupRoutes from "./routes/group.routes";
|
||||||
|
import rewardRoutes from "./routes/reward.routes";
|
||||||
|
|
||||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -42,6 +43,7 @@ app.use("/api/programs", programRoutes);
|
|||||||
app.use("/api/history", historyRoutes);
|
app.use("/api/history", historyRoutes);
|
||||||
app.use("/api/friends", friendRoutes);
|
app.use("/api/friends", friendRoutes);
|
||||||
app.use("/api/groups", groupRoutes);
|
app.use("/api/groups", groupRoutes);
|
||||||
|
app.use("/api/rewards", rewardRoutes);
|
||||||
|
|
||||||
app.get("/health", (_req, res) => {
|
app.get("/health", (_req, res) => {
|
||||||
res.json({ status: "ok" });
|
res.json({ status: "ok" });
|
||||||
|
|||||||
10
backend/src/routes/reward.routes.ts
Normal file
10
backend/src/routes/reward.routes.ts
Normal file
@@ -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;
|
||||||
76
backend/src/services/reward.service.ts
Normal file
76
backend/src/services/reward.service.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { prisma } from "../index";
|
||||||
|
|
||||||
|
// Conditions disponibles — à étendre librement
|
||||||
|
const CONDITIONS: Record<string, (userId: string) => Promise<boolean>> = {
|
||||||
|
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<string[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user