feat(simulations): persistance session — autosave + sujet_id + getById tolère rapport=null (FTD-21)
This commit is contained in:
parent
fc76fac981
commit
fcd8fe7017
3 changed files with 533 additions and 77 deletions
|
|
@ -75,29 +75,8 @@ export async function create(
|
|||
}
|
||||
}
|
||||
|
||||
// 2. Insérer dans productions
|
||||
const { data, error } = await supabase
|
||||
.from('productions')
|
||||
.insert({
|
||||
user_id: profile.id,
|
||||
tache: body.tache,
|
||||
mode: body.mode,
|
||||
contenu: body.contenu ?? null,
|
||||
})
|
||||
.select('id, tache, mode, created_at')
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Une erreur est survenue. Veuillez réessayer dans quelques instants.',
|
||||
status: 500,
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch un sujet aléatoire (non bloquant — sujet: null si introuvable).
|
||||
// TODO: migrer vers une RPC PostgreSQL si la table sujets dépasse quelques centaines de lignes.
|
||||
// 2. Fetch un sujet aléatoire AVANT l'insert pour persister sujet_id en une seule requête.
|
||||
// (non bloquant — sujet: null si introuvable).
|
||||
const sujetParams = mapTacheToSujetParams(body.tache)
|
||||
let sujet: SujetData | null = null
|
||||
if (sujetParams) {
|
||||
|
|
@ -113,6 +92,28 @@ export async function create(
|
|||
}
|
||||
}
|
||||
|
||||
// 3. Insérer dans productions avec sujet_id (FTD-21 — persistance pour resume).
|
||||
const { data, error } = await supabase
|
||||
.from('productions')
|
||||
.insert({
|
||||
user_id: profile.id,
|
||||
tache: body.tache,
|
||||
mode: body.mode,
|
||||
contenu: body.contenu ?? null,
|
||||
sujet_id: sujet?.id ?? null,
|
||||
})
|
||||
.select('id, tache, mode, created_at')
|
||||
.single()
|
||||
|
||||
if (error || !data) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Une erreur est survenue. Veuillez réessayer dans quelques instants.',
|
||||
status: 500,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
id: data.id,
|
||||
|
|
@ -126,14 +127,22 @@ export async function create(
|
|||
|
||||
// EERapport et EORapport ont la même structure depuis l'étape A —
|
||||
// on utilise EERapport comme représentation canonique du rapport parsé.
|
||||
export type GetByIdResult = EERapport & {
|
||||
//
|
||||
// FTD-21 : rapport peut être null (simulation en cours, pas encore corrigée).
|
||||
// Le frontend distingue :
|
||||
// - rapport !== null → RapportPage affiche la correction
|
||||
// - rapport === null → SimulationFlowProvider restaure la session (resume)
|
||||
export interface GetByIdResult {
|
||||
simulation_id: string
|
||||
tache: Tache
|
||||
mode: Mode
|
||||
created_at: string
|
||||
contenu: string | null
|
||||
sujet: SujetData | null
|
||||
rapport: EERapport | null
|
||||
}
|
||||
|
||||
type GetByIdError = {
|
||||
type ControllerError = {
|
||||
error: true
|
||||
code: string
|
||||
message: string
|
||||
|
|
@ -143,10 +152,10 @@ type GetByIdError = {
|
|||
export async function getById(
|
||||
id: string,
|
||||
profile: AuthProfile
|
||||
): Promise<{ data: GetByIdResult } | GetByIdError> {
|
||||
): Promise<{ data: GetByIdResult } | ControllerError> {
|
||||
const { data, error } = await supabase
|
||||
.from('productions')
|
||||
.select('id, user_id, tache, mode, score, nclc, rapport, created_at')
|
||||
.select('id, user_id, tache, mode, contenu, sujet_id, rapport, created_at')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
|
|
@ -168,24 +177,170 @@ export async function getById(
|
|||
}
|
||||
}
|
||||
|
||||
if (data.rapport === null) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'REPORT_NOT_READY',
|
||||
message: "Le rapport n'est pas encore disponible pour cette simulation.",
|
||||
status: 404,
|
||||
}
|
||||
// Charger le sujet si présent (FTD-21 — restore complet de la session).
|
||||
let sujet: SujetData | null = null
|
||||
if (data.sujet_id) {
|
||||
const { data: sujetRow } = await supabase
|
||||
.from('sujets')
|
||||
.select('id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte')
|
||||
.eq('id', data.sujet_id)
|
||||
.single()
|
||||
if (sujetRow) sujet = sujetRow as SujetData
|
||||
}
|
||||
|
||||
const rapport = JSON.parse(data.rapport) as EERapport
|
||||
const rapport = data.rapport ? (JSON.parse(data.rapport) as EERapport) : null
|
||||
|
||||
return {
|
||||
data: {
|
||||
...rapport,
|
||||
simulation_id: data.id,
|
||||
tache: data.tache as Tache,
|
||||
mode: data.mode as Mode,
|
||||
created_at: data.created_at,
|
||||
contenu: data.contenu ?? null,
|
||||
sujet,
|
||||
rapport,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FTD-21 — autosave du contenu d'une simulation en cours.
|
||||
* Refuse si la simulation est déjà corrigée (rapport !== null).
|
||||
*/
|
||||
export async function autosaveContenu(
|
||||
id: string,
|
||||
userId: string,
|
||||
contenu: string
|
||||
): Promise<{ data: { ok: true } } | ControllerError> {
|
||||
if (contenu.length > 5000) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Le texte ne doit pas dépasser 5 000 caractères.',
|
||||
status: 400,
|
||||
}
|
||||
}
|
||||
|
||||
const { data: prod, error } = await supabase
|
||||
.from('productions')
|
||||
.select('user_id, rapport')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (error || !prod) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SIMULATION_NOT_FOUND',
|
||||
message: 'Simulation introuvable.',
|
||||
status: 404,
|
||||
}
|
||||
}
|
||||
|
||||
if (prod.user_id !== userId) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: 'Cette simulation ne vous appartient pas.',
|
||||
status: 401,
|
||||
}
|
||||
}
|
||||
|
||||
if (prod.rapport !== null) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Cette simulation a déjà été corrigée.',
|
||||
status: 400,
|
||||
}
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.update({ contenu })
|
||||
.eq('id', id)
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Sauvegarde impossible. Réessayez dans quelques instants.',
|
||||
status: 500,
|
||||
}
|
||||
}
|
||||
|
||||
return { data: { ok: true } }
|
||||
}
|
||||
|
||||
/**
|
||||
* FTD-21 — met à jour le sujet d'une simulation en cours.
|
||||
* Vérifie que le sujet existe et que la simulation n'est pas corrigée.
|
||||
*/
|
||||
export async function updateSujet(
|
||||
id: string,
|
||||
userId: string,
|
||||
sujetId: string
|
||||
): Promise<{ data: { sujet: SujetData } } | ControllerError> {
|
||||
const { data: sujetRow, error: sujetError } = await supabase
|
||||
.from('sujets')
|
||||
.select('id, consigne, role, contexte, doc1_titre, doc1_texte, doc2_titre, doc2_texte')
|
||||
.eq('id', sujetId)
|
||||
.single()
|
||||
|
||||
if (sujetError || !sujetRow) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SUJET_NOT_FOUND',
|
||||
message: 'Sujet introuvable.',
|
||||
status: 404,
|
||||
}
|
||||
}
|
||||
|
||||
const { data: prod, error } = await supabase
|
||||
.from('productions')
|
||||
.select('user_id, rapport')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (error || !prod) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'SIMULATION_NOT_FOUND',
|
||||
message: 'Simulation introuvable.',
|
||||
status: 404,
|
||||
}
|
||||
}
|
||||
|
||||
if (prod.user_id !== userId) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'AUTH_REQUIRED',
|
||||
message: 'Cette simulation ne vous appartient pas.',
|
||||
status: 401,
|
||||
}
|
||||
}
|
||||
|
||||
if (prod.rapport !== null) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Cette simulation a déjà été corrigée.',
|
||||
status: 400,
|
||||
}
|
||||
}
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('productions')
|
||||
.update({ sujet_id: sujetId })
|
||||
.eq('id', id)
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
error: true,
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Mise à jour impossible. Réessayez dans quelques instants.',
|
||||
status: 500,
|
||||
}
|
||||
}
|
||||
|
||||
return { data: { sujet: sujetRow as SujetData } }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue