Compare commits
1 Commits
feat/phase
...
feat/phase
| Author | SHA1 | Date | |
|---|---|---|---|
| 2389376721 |
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `programId` to the `histories` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "histories" ADD COLUMN "completedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "programId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "histories" ADD CONSTRAINT "histories_programId_fkey" FOREIGN KEY ("programId") REFERENCES "programs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -88,6 +88,7 @@ model Program {
|
|||||||
|
|
||||||
exercises ProgramExercise[]
|
exercises ProgramExercise[]
|
||||||
groups GroupProgram[]
|
groups GroupProgram[]
|
||||||
|
histories History[]
|
||||||
|
|
||||||
@@map("programs")
|
@@map("programs")
|
||||||
}
|
}
|
||||||
@@ -111,12 +112,15 @@ model ProgramExercise {
|
|||||||
// ─── History ─────────────────────────────────────────────────────────────────
|
// ─── History ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
model History {
|
model History {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
date DateTime @default(now())
|
date DateTime @default(now())
|
||||||
notes String?
|
completedAt DateTime?
|
||||||
|
notes String?
|
||||||
|
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
programId String
|
||||||
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
entries HistoryEntry[]
|
entries HistoryEntry[]
|
||||||
|
|
||||||
|
|||||||
123
backend/src/controllers/history.controller.ts
Normal file
123
backend/src/controllers/history.controller.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { Response } from "express";
|
||||||
|
import { prisma } from "../index";
|
||||||
|
import type { AppRequest } from "../types/context";
|
||||||
|
import {
|
||||||
|
addEntrySchema,
|
||||||
|
completeHistorySchema,
|
||||||
|
startHistorySchema,
|
||||||
|
} from "../validators/history.validators";
|
||||||
|
|
||||||
|
// Démarre une session d'entraînement
|
||||||
|
export async function start(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const parsed = startHistorySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = await prisma.program.findFirst({
|
||||||
|
where: {
|
||||||
|
id: parsed.data.programId,
|
||||||
|
OR: [{ isPublic: true }, { authorId: req.user!.id }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!program) {
|
||||||
|
res.status(404).json({ message: "Programme introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await prisma.history.create({
|
||||||
|
data: {
|
||||||
|
userId: req.user!.id,
|
||||||
|
programId: parsed.data.programId,
|
||||||
|
},
|
||||||
|
include: { entries: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajoute un exercice complété à la session
|
||||||
|
export async function addEntry(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const historyId = req.params.id as string;
|
||||||
|
const parsed = addEntrySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await prisma.history.findFirst({
|
||||||
|
where: { id: historyId, userId: req.user!.id, completedAt: null },
|
||||||
|
});
|
||||||
|
if (!history) {
|
||||||
|
res.status(404).json({ message: "Session introuvable ou déjà terminée." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await prisma.historyEntry.create({
|
||||||
|
data: { historyId, ...parsed.data },
|
||||||
|
include: { exercise: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Termine la session
|
||||||
|
export async function complete(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const historyId = req.params.id as string;
|
||||||
|
const parsed = completeHistorySchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({ message: parsed.error.issues[0].message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = await prisma.history.findFirst({
|
||||||
|
where: { id: historyId, userId: req.user!.id, completedAt: null },
|
||||||
|
include: { entries: true },
|
||||||
|
});
|
||||||
|
if (!history) {
|
||||||
|
res.status(404).json({ message: "Session introuvable ou déjà terminée." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = await prisma.history.update({
|
||||||
|
where: { id: historyId },
|
||||||
|
data: {
|
||||||
|
completedAt: new Date(),
|
||||||
|
notes: parsed.data.notes,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
entries: { include: { exercise: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(completed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Historique de l'utilisateur connecté
|
||||||
|
export async function getMyHistory(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const histories = await prisma.history.findMany({
|
||||||
|
where: { userId: req.user!.id },
|
||||||
|
include: {
|
||||||
|
entries: { include: { exercise: true } },
|
||||||
|
},
|
||||||
|
orderBy: { date: "desc" },
|
||||||
|
});
|
||||||
|
res.json(histories);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Détail d'une session
|
||||||
|
export async function getOne(req: AppRequest, res: Response): Promise<void> {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const history = await prisma.history.findFirst({
|
||||||
|
where: { id, userId: req.user!.id },
|
||||||
|
include: {
|
||||||
|
entries: { include: { exercise: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!history) {
|
||||||
|
res.status(404).json({ message: "Session introuvable." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(history);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import authRoutes from "./routes/auth.routes";
|
|||||||
import userRoutes from "./routes/user.routes";
|
import userRoutes from "./routes/user.routes";
|
||||||
import exerciseRoutes from "./routes/exercise.routes";
|
import exerciseRoutes from "./routes/exercise.routes";
|
||||||
import programRoutes from "./routes/program.routes";
|
import programRoutes from "./routes/program.routes";
|
||||||
|
import historyRoutes from "./routes/history.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();
|
||||||
@@ -36,9 +37,9 @@ app.use("/api/auth", authRoutes);
|
|||||||
app.use("/api/users", userRoutes);
|
app.use("/api/users", userRoutes);
|
||||||
app.use("/api/exercises", exerciseRoutes);
|
app.use("/api/exercises", exerciseRoutes);
|
||||||
app.use("/api/programs", programRoutes);
|
app.use("/api/programs", programRoutes);
|
||||||
|
app.use("/api/history", historyRoutes);
|
||||||
// app.use("/api/groups", groupRoutes);
|
// app.use("/api/groups", groupRoutes);
|
||||||
// app.use("/api/friends", friendRoutes);
|
// app.use("/api/friends", friendRoutes);
|
||||||
// app.use("/api/history", historyRoutes);
|
|
||||||
|
|
||||||
app.get("/health", (_req, res) => {
|
app.get("/health", (_req, res) => {
|
||||||
res.json({ status: "ok" });
|
res.json({ status: "ok" });
|
||||||
|
|||||||
13
backend/src/routes/history.routes.ts
Normal file
13
backend/src/routes/history.routes.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { start, addEntry, complete, getMyHistory, getOne } from "../controllers/history.controller";
|
||||||
|
import { requireAuth } from "../middlewares/auth";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/", requireAuth, getMyHistory);
|
||||||
|
router.get("/:id", requireAuth, getOne);
|
||||||
|
router.post("/", requireAuth, start);
|
||||||
|
router.post("/:id/entries", requireAuth, addEntry);
|
||||||
|
router.patch("/:id/complete", requireAuth, complete);
|
||||||
|
|
||||||
|
export default router;
|
||||||
21
backend/src/validators/history.validators.ts
Normal file
21
backend/src/validators/history.validators.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const startHistorySchema = z.object({
|
||||||
|
programId: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addEntrySchema = z.object({
|
||||||
|
exerciseId: z.string().min(1),
|
||||||
|
sets: z.number().int().min(1),
|
||||||
|
reps: z.number().int().min(1).optional(),
|
||||||
|
weightKg: z.number().min(0).optional(),
|
||||||
|
durationSec: z.number().int().min(1).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const completeHistorySchema = z.object({
|
||||||
|
notes: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StartHistoryInput = z.infer<typeof startHistorySchema>;
|
||||||
|
export type AddEntryInput = z.infer<typeof addEntrySchema>;
|
||||||
|
export type CompleteHistoryInput = z.infer<typeof completeHistorySchema>;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const programExerciseSchema = z.object({
|
const programExerciseSchema = z.object({
|
||||||
exerciseId: z.string().uuid(),
|
exerciseId: z.string().min(1),
|
||||||
sets: z.number().int().min(1),
|
sets: z.number().int().min(1),
|
||||||
reps: z.number().int().min(1).optional(),
|
reps: z.number().int().min(1).optional(),
|
||||||
durationSec: z.number().int().min(1).optional(),
|
durationSec: z.number().int().min(1).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user