diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e35a009..3d69a3b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -8,6 +8,20 @@ const SILENCE_THRESHOLD = 0.012 const DEBUG_AUDIO = false const TTS_PROFILE_STORAGE_KEY = 'professeur-top-tts-profile' +const REVIEW_STATUS_OPTIONS = [ + { value: 'needs_changes', label: 'A corriger' }, + { value: 'to_review', label: 'A valider' }, + { value: 'treated', label: 'Traite' }, + { value: 'archived', label: 'Archive' }, + { value: 'validated', label: 'Valide' }, + { value: 'blocked', label: 'Bloque' }, + { value: 'draft', label: 'Brouillon' }, +] + +const REVIEW_ACTIVE_STATUSES = new Set(['needs_changes', 'to_review', 'blocked', 'draft', 'unreviewed']) +const REVIEW_NODE_TYPES = ['lesson', 'fiche', 'card', 'kit', 'diagnostic', 'parcours', 'support'] +const GRADE_SLUGS = new Set(['cp', 'ce1', 'ce2', 'cm1', 'cm2', '6e', '5e', '4e', '3e']) + async function parseApiResponse(res) { const contentType = res.headers.get('content-type') || '' const bodyText = await res.text() @@ -117,6 +131,74 @@ function collectCollapsibleProgramKeys(node, depth = 0, keys = []) { return keys } +function reviewStatusLabel(status) { + return REVIEW_STATUS_OPTIONS.find((item) => item.value === status)?.label || 'A relire' +} + +function titleFromProgramSlug(value) { + if (!value) return '' + return value + .replace(/^cycle_/, 'Cycle ') + .replace(/_/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()) +} + +function getProgramNodeReviewMeta(node) { + const rawPath = node?.path || node?.key || '' + const parts = rawPath.split(/[\/#]/).filter(Boolean) + const cycle = parts.find((part) => part.startsWith('cycle_')) || '' + const grade = parts.find((part) => GRADE_SLUGS.has(part.toLowerCase())) || '' + const subject = parts.find((part) => ['mathematiques', 'francais', 'langues', 'emc'].includes(part.toLowerCase())) || '' + const status = node?.review?.status || 'unreviewed' + + return { + cycle, + cycleLabel: titleFromProgramSlug(cycle), + grade: grade.toUpperCase(), + subject, + subjectLabel: titleFromProgramSlug(subject), + status, + statusLabel: reviewStatusLabel(status), + type: node?.type || '', + } +} + +function exportReviewRows(rows, filters) { + const exportedAt = new Date() + const payload = { + schema: 'professeur-top-content-reviews-v1', + exported_at: exportedAt.toISOString(), + filters, + count: rows.length, + reviews: rows.map(({ node, meta }) => ({ + node_key: node.key, + path: node.path || node.key, + label: node.label, + type: node.type, + cycle: meta.cycle, + grade: meta.grade, + subject: meta.subject, + status: meta.status, + status_label: meta.statusLabel, + comment: node.review?.comment || '', + reviewer: node.review?.reviewer || '', + updated_at: node.review?.updated_at || '', + has_visual: Boolean(node.url), + has_text_preview: Boolean(node.content), + })), + } + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const anchor = document.createElement('a') + const datePart = exportedAt.toISOString().slice(0, 10) + anchor.href = url + anchor.download = `revues-contenus-${datePart}.json` + document.body.appendChild(anchor) + anchor.click() + anchor.remove() + URL.revokeObjectURL(url) +} + function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveReview, readOnly = false, onOpenValidation }) { const [isFullscreen, setIsFullscreen] = useState(false) @@ -191,10 +273,9 @@ function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveRe value={reviewForm.status} onChange={(event) => onReviewChange({ ...reviewForm, status: event.target.value })} > - - - - + {REVIEW_STATUS_OPTIONS.map((option) => ( + + ))}