diff --git a/backend/src/database/initdb/initQcmvss.ts b/backend/src/database/initdb/initQcmvss.ts index 7137dcc..798683b 100644 --- a/backend/src/database/initdb/initQcmvss.ts +++ b/backend/src/database/initdb/initQcmvss.ts @@ -1,13 +1,16 @@ +import { and, eq, sql } from 'drizzle-orm'; import { db } from '../db'; import { vssqcmanswerSchema } from '../../schemas/Relational/vssqcmanswer.schema'; import { vssqcmquestionSchema } from '../../schemas/Basic/vssqcmquestion.schema'; type SeedQuestion = { question: string; + questionEn: string; points: number; type: 'single_choice' | 'multiple_choice'; answers: { answer: string; + answerEn: string; is_correct: boolean; }[]; }; @@ -15,177 +18,344 @@ type SeedQuestion = { const qcmQuestions: SeedQuestion[] = [ { question: 'Oui = ', + questionEn: 'Yes = ', points: 1, type: 'single_choice', answers: [ - { answer: 'Non', is_correct: false }, - { answer: 'Toujours oui', is_correct: false }, - { answer: 'Peut-être non plus tard', is_correct: true }, + { answer: 'Non', answerEn: 'No', is_correct: false }, + { answer: 'Toujours oui', answerEn: 'Always yes', is_correct: false }, + { answer: 'Peut-être non plus tard', answerEn: 'Maybe no later', is_correct: true }, ], }, { question: 'Non = ', + questionEn: 'No = ', points: 1, type: 'single_choice', answers: [ - { answer: 'Oui', is_correct: false }, - { answer: 'Non', is_correct: true }, - { answer: "Peut-être oui si j'insiste", is_correct: false }, + { answer: 'Oui', answerEn: 'Yes', is_correct: false }, + { answer: 'Non', answerEn: 'No', is_correct: true }, + { answer: "Peut-être oui si j'insiste", answerEn: 'Maybe yes if I keep insisting', is_correct: false }, ], }, { question: 'En résumé, le consentement', + questionEn: 'In short, consent', points: 2, type: 'multiple_choice', answers: [ - { answer: 'concerne une action précise', is_correct: true }, - { answer: "ne peut-être considéré comme éclairé venant d'un personne en état d'ébriété", is_correct: true }, - { answer: 'doit être libre et éclairé', is_correct: true }, - { answer: 'peut être retiré à tout moment', is_correct: true }, + { answer: 'concerne une action précise', answerEn: 'concerns a specific act', is_correct: true }, + { + answer: "ne peut-être considéré comme éclairé venant d'un personne en état d'ébriété", + answerEn: 'cannot be considered informed when coming from an intoxicated person', + is_correct: true, + }, + { answer: 'doit être libre et éclairé', answerEn: 'must be free and informed', is_correct: true }, + { answer: 'peut être retiré à tout moment', answerEn: 'can be withdrawn at any time', is_correct: true }, { answer: "spécifique, enthousiaste; valable quand la personne chancèle sous l'effet de l'alcool", + answerEn: 'specific, enthusiastic; valid when the person is staggering from alcohol', + is_correct: false, + }, + { answer: "peut s'obtenir en insistant", answerEn: 'can be obtained by insisting', is_correct: false }, + { + answer: 'est valable quand la personne est bourrée', + answerEn: 'is valid when the person is drunk', is_correct: false, }, - { answer: "peut s'obtenir en insistant", is_correct: false }, - { answer: 'est valable quand la personne est bourrée', is_correct: false }, ], }, { question: "B a embrassé A de force. B était complètement bourré. Il s'agit d'une agression sexuelle. La prise d'alcool est alors une condition :", + questionEn: + 'B forced a kiss on A. B was completely drunk. It is a sexual assault. Alcohol consumption is then a:', points: 1, type: 'single_choice', answers: [ - { answer: 'Aggravante', is_correct: true }, - { answer: 'Atténuante', is_correct: false }, + { answer: 'Aggravante', answerEn: 'Aggravating factor', is_correct: true }, + { answer: 'Atténuante', answerEn: 'Mitigating factor', is_correct: false }, ], }, { question: 'Parmi les situations suivantes, lesquelles sont des agressions sexuelles :', + questionEn: 'Which of the following situations are sexual assaults:', points: 1, type: 'multiple_choice', answers: [ - { answer: "Se frotter à quelqu'un•e", is_correct: true }, - { answer: 'Caresser les fesses de son•sa partenaire endormi•e', is_correct: true }, - { answer: "Embrasser quelqu'un•e de force", is_correct: true }, - { answer: "Embrasser par surprise quelqu'un•e qui danse au milieu de la foule", is_correct: true }, - { answer: "Embrasser quelqu'un•e tant alcoolisé•e qu'iel vient de vomir", is_correct: true }, + { answer: "Se frotter à quelqu'un•e", answerEn: 'Rubbing against someone', is_correct: true }, + { + answer: 'Caresser les fesses de son•sa partenaire endormi•e', + answerEn: 'Caressing your sleeping partner’s buttocks', + is_correct: true, + }, + { answer: "Embrasser quelqu'un•e de force", answerEn: 'Kissing someone by force', is_correct: true }, + { + answer: "Embrasser par surprise quelqu'un•e qui danse au milieu de la foule", + answerEn: 'Kissing someone by surprise who is dancing in the middle of the crowd', + is_correct: true, + }, + { + answer: "Embrasser quelqu'un•e tant alcoolisé•e qu'iel vient de vomir", + answerEn: 'Kissing someone so drunk that they have just vomited', + is_correct: true, + }, ], }, { question: "Un.e de tes amis touche les fesses de B et l'enlace. B a un mouvement de recul. Que peux-tu faire ?", + questionEn: 'One of your friends touches B’s buttocks and hugs them. B steps back. What can you do?', points: 1, type: 'multiple_choice', answers: [ - { answer: "Rien de particulier. B ne s'en souviendra sûrement pas.", is_correct: false }, - { answer: 'Demander à B si elle•il va bien', is_correct: true }, + { + answer: "Rien de particulier. B ne s'en souviendra sûrement pas.", + answerEn: 'Nothing special. B probably will not remember it anyway.', + is_correct: false, + }, + { answer: 'Demander à B si elle•il va bien', answerEn: 'Ask B if they are okay', is_correct: true }, { answer: "Prendre cet•te ami•e à part et lui faire comprendre qu'il•elle a mal agi, que B n'avait pas envie d'être touché•e.", + answerEn: + 'Take that friend aside and make them understand they acted badly, that B did not want to be touched.', + is_correct: true, + }, + { answer: 'Eloigner ton ami•e de B', answerEn: 'Move your friend away from B', is_correct: true }, + { + answer: "Le signaler à un tiers si tu penses que B peut avoir besoin d'aide", + answerEn: 'Report it to a third party if you think B may need help', is_correct: true, }, - { answer: 'Eloigner ton ami•e de B', is_correct: true }, - { answer: "Le signaler à un tiers si tu penses que B peut avoir besoin d'aide", is_correct: true }, ], }, { question: "A qui et où peux-tu demander de l'aide si tu en as besoin ?", + questionEn: 'Who can you ask for help, and where, if you need it?', points: 1, type: 'multiple_choice', answers: [ - { answer: 'Dans une zone dédiée lors des soirées, appelée la Safe Zone', is_correct: true }, - { answer: 'Au stand de prévention', is_correct: true }, - { answer: 'A la team prévention', is_correct: true }, - { answer: "Aux organisateurs de l'intégration (en t-shirt touge)", is_correct: true }, - { answer: "A tes chefs d'équipe", is_correct: true }, - { answer: 'A ta marraine UTTienne/ A ton parrain UTTien', is_correct: true }, - { answer: 'À un•e ami•e', is_correct: true }, + { + answer: 'Dans une zone dédiée lors des soirées, appelée la Safe Zone', + answerEn: 'In a dedicated area during parties, called the Safe Zone', + is_correct: true, + }, + { answer: 'Au stand de prévention', answerEn: 'At the prevention booth', is_correct: true }, + { answer: 'A la team prévention', answerEn: 'To the prevention team', is_correct: true }, + { + answer: "Aux organisateurs de l'intégration (en t-shirt touge)", + answerEn: 'To the integration organizers (wearing red T-shirts)', + is_correct: true, + }, + { answer: "A tes chefs d'équipe", answerEn: 'To your team leaders', is_correct: true }, + { + answer: 'A ta marraine UTTienne/ A ton parrain UTTien', + answerEn: 'To your UTT mentor / buddy', + is_correct: true, + }, + { answer: 'À un•e ami•e', answerEn: 'To a friend', is_correct: true }, ], }, { question: 'En cas de VSS, quelles sont les peines maximales légalement encourue par une personne ayant commis une agression sexuelle ?', + questionEn: + 'In a VSS case, what is the maximum legally punishable sentence for a person who committed sexual assault?', points: 1, type: 'single_choice', answers: [ - { answer: "75 000 € d'amende et 5 ans d'emprisonnement", is_correct: true }, - { answer: "10 000€ d'amende", is_correct: false }, - { answer: '15 ans de prison', is_correct: false }, + { + answer: "75 000 € d'amende et 5 ans d'emprisonnement", + answerEn: 'A €75,000 fine and 5 years in prison', + is_correct: true, + }, + { answer: "10 000€ d'amende", answerEn: 'A €10,000 fine', is_correct: false }, + { answer: '15 ans de prison', answerEn: '15 years in prison', is_correct: false }, ], }, { question: 'Quelles sont les conséquences possibles pour la victime de VSS ?', + questionEn: 'What are the possible consequences for a VSS victim?', points: 1, type: 'multiple_choice', answers: [ - { answer: 'Aucun effet particulier', is_correct: false }, - { answer: 'Problèmes somatiques (nausées, migraines, fatigue)', is_correct: true }, - { answer: 'Dysfonction sexuelle', is_correct: true }, - { answer: "Crainte de l'intimité", is_correct: true }, - { answer: 'Dépression majeure', is_correct: true }, - { answer: 'Détresse psychologique', is_correct: true }, + { answer: 'Aucun effet particulier', answerEn: 'No particular effect', is_correct: false }, + { + answer: 'Problèmes somatiques (nausées, migraines, fatigue)', + answerEn: 'Somatic problems (nausea, migraines, fatigue)', + is_correct: true, + }, + { answer: 'Dysfonction sexuelle', answerEn: 'Sexual dysfunction', is_correct: true }, + { answer: "Crainte de l'intimité", answerEn: 'Fear of intimacy', is_correct: true }, + { answer: 'Dépression majeure', answerEn: 'Major depression', is_correct: true }, + { answer: 'Détresse psychologique', answerEn: 'Psychological distress', is_correct: true }, ], }, { question: 'Que puis-je faire si je suis témoins de VSS ?', + questionEn: 'What can I do if I witness VSS?', points: 1, type: 'multiple_choice', answers: [ - { answer: "Dire à la victime de faire attention à elle et de mieux s'habiller", is_correct: false }, - { answer: 'Aller voir la team prévention ou les super-orgas', is_correct: true }, - { answer: 'Appeler France Victime (01 80 52 33 86)', is_correct: true }, - { answer: "Appeler le numéro d'astreinte", is_correct: true }, + { + answer: "Dire à la victime de faire attention à elle et de mieux s'habiller", + answerEn: 'Tell the victim to be careful and dress better', + is_correct: false, + }, + { + answer: 'Aller voir la team prévention ou les super-orgas', + answerEn: 'Go see the prevention team or the super-organizers', + is_correct: true, + }, + { + answer: 'Appeler France Victime (01 80 52 33 86)', + answerEn: 'Call France Victime (01 80 52 33 86)', + is_correct: true, + }, + { answer: "Appeler le numéro d'astreinte", answerEn: 'Call the on-call number', is_correct: true }, ], }, { question: "A quelle sentence s'expose une personne commettant un viol ? \n Article 222-23 Version en vigueur depuis le 23 avril 2021 \n Tout acte de pénétration sexuelle, de quelque nature qu'il soit, ou tout acte bucco-génital commis sur la personne d'autrui ou sur la personne de l'auteur par violence, contrainte, menace ou surprise est un viol. \n Le viol est puni de quinze ans de réclusion criminelle.", + questionEn: + 'What sentence does a person committing rape face?\n Article 222-23 Valid since April 23, 2021 \n Any act of sexual penetration, of whatever nature, or any oral-genital act committed on another person or on the perpetrator by violence, coercion, threat, or surprise is rape. \n Rape is punishable by fifteen years of criminal imprisonment.', points: 1, type: 'multiple_choice', answers: [ - { answer: '15 ans de réclusion criminelle', is_correct: true }, - { answer: "100 000€ d'amence et 20 ans de réclusion criminelle", is_correct: false }, - { answer: "100 000€ d'amence et 10 ans de réclusion criminelle", is_correct: false }, + { + answer: '15 ans de réclusion criminelle', + answerEn: '15 years of criminal imprisonment', + is_correct: true, + }, + { + answer: "100 000€ d'amence et 20 ans de réclusion criminelle", + answerEn: 'A €100,000 fine and 20 years of criminal imprisonment', + is_correct: false, + }, + { + answer: "100 000€ d'amence et 10 ans de réclusion criminelle", + answerEn: 'A €100,000 fine and 10 years of criminal imprisonment', + is_correct: false, + }, ], }, { question: "Qu'est-ce qui est considéré comme un acte de bizutage (et qui est donc interdit) ? ", + questionEn: 'What is considered hazing (and is therefore prohibited)? ', points: 1, type: 'multiple_choice', answers: [ - { answer: "Se dénuder ou inciter quelqu'un à se dénuder (Limousin, Maréchal...)", is_correct: true }, - { answer: "Obliger quelqu'un à boire de l'alcool de force lors d'une soirée", is_correct: true }, + { + answer: "Se dénuder ou inciter quelqu'un à se dénuder (Limousin, Maréchal...)", + answerEn: 'Undressing or forcing someone to undress (Limousin, Maréchal...)', + is_correct: true, + }, + { + answer: "Obliger quelqu'un à boire de l'alcool de force lors d'une soirée", + answerEn: 'Forcing someone to drink alcohol at a party', + is_correct: true, + }, { answer: 'Organiser une chasse au trésor géante à travers toute la ville pour les nouveaux', + answerEn: 'Organizing a giant treasure hunt across the whole city for the newcomers', + is_correct: false, + }, + { + answer: 'Humilier publiquement un nouveau devant le groupe', + answerEn: 'Publicly humiliating a newcomer in front of the group', + is_correct: true, + }, + { + answer: 'Forcer une personne à effectuer des tâches dégradantes ou dangereuses', + answerEn: 'Forcing a person to perform degrading or dangerous tasks', + is_correct: true, + }, + { + answer: "Défier un nouveau à réciter l'annuaire téléphonique en dansant la macarena", + answerEn: 'Challenging a newcomer to recite the phone book while dancing the Macarena', + is_correct: false, + }, + { + answer: 'Motiver les nouveaux à se déguiser en canard', + answerEn: 'Encouraging newcomers to dress up as a duck', is_correct: false, }, - { answer: 'Humilier publiquement un nouveau devant le groupe', is_correct: true }, - { answer: 'Forcer une personne à effectuer des tâches dégradantes ou dangereuses', is_correct: true }, - { answer: "Défier un nouveau à réciter l'annuaire téléphonique en dansant la macarena", is_correct: false }, - { answer: 'Motiver les nouveaux à se déguiser en canard', is_correct: false }, - { answer: 'Forcer les nouveaux à porter un déguisement obscène', is_correct: true }, + { + answer: 'Forcer les nouveaux à porter un déguisement obscène', + answerEn: 'Forcing newcomers to wear an obscene costume', + is_correct: true, + }, ], }, { question: "A quelles sanctions s'expose l'auteur du bizutage ?", + questionEn: 'What penalties does the person responsible for hazing face?', points: 1, type: 'multiple_choice', answers: [ - { answer: "Une exclusion de l'intégration", is_correct: true }, - { answer: 'Rien du tout', is_correct: false }, + { + answer: "Une exclusion de l'intégration", + answerEn: 'An exclusion from the integration', + is_correct: true, + }, + { answer: 'Rien du tout', answerEn: 'Nothing at all', is_correct: false }, { answer: "Le bizutage est un délit. Il est puni de 6 mois d'emprisonnement et de 7 500 € d'amende.", + answerEn: 'Hazing is a criminal offense. It is punishable by 6 months in prison and a €7,500 fine.', + is_correct: true, + }, + { + answer: 'Si la victime est une personne vulnérable, les peines sont doublées', + answerEn: 'If the victim is a vulnerable person, the penalties are doubled', is_correct: true, }, - { answer: 'Si la victime est une personne vulnérable, les peines sont doublées', is_correct: true }, - { answer: 'Une mauvaise note', is_correct: false }, + { answer: 'Une mauvaise note', answerEn: 'A bad grade', is_correct: false }, ], }, ]; +const ensureTranslationColumns = async () => { + await db.execute(sql`ALTER TABLE "vssqcmquestion" ADD COLUMN IF NOT EXISTS "question_en" text;`); + await db.execute(sql`ALTER TABLE "vssqcmanswer" ADD COLUMN IF NOT EXISTS "answer_en" text;`); +}; + +const updateQuestionTranslations = async () => { + for (const seedQuestion of qcmQuestions) { + await db + .update(vssqcmquestionSchema) + .set({ question_en: seedQuestion.questionEn }) + .where(eq(vssqcmquestionSchema.question, seedQuestion.question)); + + const [question] = await db + .select({ id: vssqcmquestionSchema.id }) + .from(vssqcmquestionSchema) + .where(eq(vssqcmquestionSchema.question, seedQuestion.question)); + + if (!question) { + continue; + } + + for (const seedAnswer of seedQuestion.answers) { + await db + .update(vssqcmanswerSchema) + .set({ answer_en: seedAnswer.answerEn }) + .where( + and( + eq(vssqcmanswerSchema.questionid, question.id), + eq(vssqcmanswerSchema.answer, seedAnswer.answer), + ), + ); + } + } +}; + export const initQcmvss = async () => { + await ensureTranslationColumns(); + const existingQuestion = await db.select().from(vssqcmquestionSchema).limit(1); if (existingQuestion.length > 0) { + await updateQuestionTranslations(); return; } @@ -194,6 +364,7 @@ export const initQcmvss = async () => { .insert(vssqcmquestionSchema) .values({ question: seedQuestion.question, + question_en: seedQuestion.questionEn, points: seedQuestion.points, type: seedQuestion.type, }) @@ -207,6 +378,7 @@ export const initQcmvss = async () => { seedQuestion.answers.map((seedAnswer) => ({ questionid: createdQuestion.id, answer: seedAnswer.answer, + answer_en: seedAnswer.answerEn, is_correct: seedAnswer.is_correct, })), ); diff --git a/backend/src/database/migrations/0027_faulty_hellion.sql b/backend/src/database/migrations/0027_faulty_hellion.sql new file mode 100644 index 0000000..9cfca1d --- /dev/null +++ b/backend/src/database/migrations/0027_faulty_hellion.sql @@ -0,0 +1,2 @@ +ALTER TABLE "vssqcmquestion" ADD COLUMN "question_en" text;--> statement-breakpoint +ALTER TABLE "vssqcmanswer" ADD COLUMN "answer_en" text; \ No newline at end of file diff --git a/backend/src/database/migrations/meta/0027_snapshot.json b/backend/src/database/migrations/meta/0027_snapshot.json new file mode 100644 index 0000000..08603d4 --- /dev/null +++ b/backend/src/database/migrations/meta/0027_snapshot.json @@ -0,0 +1,1509 @@ +{ + "id": "6179fb88-23bd-4656-9980-faa52a9d9b24", + "prevId": "af2f7f68-b5fb-4945-87a6-dcf6b539c002", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.banned_addresses": { + "name": "banned_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "banned_addresses_email_unique": { + "name": "banned_addresses_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenges": { + "name": "challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "challenges_created_by_users_id_fk": { + "name": "challenges_created_by_users_id_fk", + "tableFrom": "challenges", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pre_registration_open": { + "name": "pre_registration_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "shotgun_open": { + "name": "shotgun_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sdi_open": { + "name": "sdi_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wei_open": { + "name": "wei_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "food_open": { + "name": "food_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "chall_open": { + "name": "chall_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.factions": { + "name": "factions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "factions_name_unique": { + "name": "factions_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.news": { + "name": "news", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permanences": { + "name": "permanences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_at": { + "name": "start_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_at": { + "name": "end_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_open": { + "name": "is_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socialLink": { + "name": "socialLink", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "riCompatible": { + "name": "riCompatible", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_name_unique": { + "name": "teams_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "majeur": { + "name": "majeur", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "male": { + "name": "male", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Nouveau'" + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "vss_form": { + "name": "vss_form", + "type": "vss_form", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vssqcmquestion": { + "name": "vssqcmquestion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "question_en": { + "name": "question_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "question_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bus_attribution": { + "name": "bus_attribution", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bus": { + "name": "bus", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "departure_time": { + "name": "departure_time", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bus_attribution_user_id_users_id_fk": { + "name": "bus_attribution_user_id_users_id_fk", + "tableFrom": "bus_attribution", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenge_validation": { + "name": "challenge_validation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "challenge_id": { + "name": "challenge_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validated_by_admin_id": { + "name": "validated_by_admin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validated_at": { + "name": "validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "target_user_id": { + "name": "target_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "target_team_id": { + "name": "target_team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "target_faction_id": { + "name": "target_faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "added_by_admin_id": { + "name": "added_by_admin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "challenge_validation_challenge_id_challenges_id_fk": { + "name": "challenge_validation_challenge_id_challenges_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "challenges", + "columnsFrom": [ + "challenge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_validated_by_admin_id_users_id_fk": { + "name": "challenge_validation_validated_by_admin_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "validated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_user_id_users_id_fk": { + "name": "challenge_validation_target_user_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_team_id_teams_id_fk": { + "name": "challenge_validation_target_team_id_teams_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "teams", + "columnsFrom": [ + "target_team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_target_faction_id_factions_id_fk": { + "name": "challenge_validation_target_faction_id_factions_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "factions", + "columnsFrom": [ + "target_faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_validation_added_by_admin_id_users_id_fk": { + "name": "challenge_validation_added_by_admin_id_users_id_fk", + "tableFrom": "challenge_validation", + "tableTo": "users", + "columnsFrom": [ + "added_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_tokens": { + "name": "registration_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "registration_tokens_user_id_users_id_fk": { + "name": "registration_tokens_user_id_users_id_fk", + "tableFrom": "registration_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registration_tokens_token_unique": { + "name": "registration_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_points": { + "name": "role_points", + "schema": "", + "columns": { + "role_points": { + "name": "role_points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_points_role_points_roles_id_fk": { + "name": "role_points_role_points_roles_id_fk", + "tableFrom": "role_points", + "tableTo": "roles", + "columnsFrom": [ + "role_points" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_points_role_points_pk": { + "name": "role_points_role_points_pk", + "columns": [ + "role_points" + ] + } + }, + "uniqueConstraints": { + "role_points_role_points_unique": { + "name": "role_points_role_points_unique", + "nullsNotDistinct": false, + "columns": [ + "role_points" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_faction": { + "name": "team_faction", + "schema": "", + "columns": { + "faction_id": { + "name": "faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "team_faction_faction_id_factions_id_fk": { + "name": "team_faction_faction_id_factions_id_fk", + "tableFrom": "team_faction", + "tableTo": "factions", + "columnsFrom": [ + "faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_faction_team_id_teams_id_fk": { + "name": "team_faction_team_id_teams_id_fk", + "tableFrom": "team_faction", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "team_faction_faction_id_team_id_pk": { + "name": "team_faction_faction_id_team_id_pk", + "columns": [ + "faction_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_shotgun": { + "name": "team_shotgun", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_shotgun_team_id_teams_id_fk": { + "name": "team_shotgun_team_id_teams_id_fk", + "tableFrom": "team_shotgun", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_informations": { + "name": "user_informations", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "emergency_contact_name": { + "name": "emergency_contact_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emergency_contact_phone": { + "name": "emergency_contact_phone", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_informations_user_id_users_id_fk": { + "name": "user_informations_user_id_users_id_fk", + "tableFrom": "user_informations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.respo_permanences": { + "name": "respo_permanences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "permanence_id": { + "name": "permanence_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "respo_permanences_user_id_users_id_fk": { + "name": "respo_permanences_user_id_users_id_fk", + "tableFrom": "respo_permanences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "respo_permanences_permanence_id_permanences_id_fk": { + "name": "respo_permanences_permanence_id_permanences_id_fk", + "tableFrom": "respo_permanences", + "tableTo": "permanences", + "columnsFrom": [ + "permanence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "respo_permanences_user_id_permanence_id_pk": { + "name": "respo_permanences_user_id_permanence_id_pk", + "columns": [ + "user_id", + "permanence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_permanences": { + "name": "user_permanences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "permanence_id": { + "name": "permanence_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "claimed": { + "name": "claimed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_permanences_user_id_users_id_fk": { + "name": "user_permanences_user_id_users_id_fk", + "tableFrom": "user_permanences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_permanences_permanence_id_permanences_id_fk": { + "name": "user_permanences_permanence_id_permanences_id_fk", + "tableFrom": "user_permanences", + "tableTo": "permanences", + "columnsFrom": [ + "permanence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_permanences_user_id_permanence_id_pk": { + "name": "user_permanences_user_id_permanence_id_pk", + "columns": [ + "user_id", + "permanence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_role_id_roles_id_fk": { + "name": "user_preferences_role_id_roles_id_fk", + "tableFrom": "user_preferences", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id_role_id_pk": { + "name": "user_preferences_user_id_role_id_pk", + "columns": [ + "user_id", + "role_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_teams": { + "name": "user_teams", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_teams_user_id_users_id_fk": { + "name": "user_teams_user_id_users_id_fk", + "tableFrom": "user_teams", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_teams_team_id_teams_id_fk": { + "name": "user_teams_team_id_teams_id_fk", + "tableFrom": "user_teams", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_teams_user_id_team_id_pk": { + "name": "user_teams_user_id_team_id_pk", + "columns": [ + "user_id", + "team_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_tent": { + "name": "user_tent", + "schema": "", + "columns": { + "user_id_1": { + "name": "user_id_1", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id_2": { + "name": "user_id_2", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "confirmed": { + "name": "confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_tent_user_id_1_users_id_fk": { + "name": "user_tent_user_id_1_users_id_fk", + "tableFrom": "user_tent", + "tableTo": "users", + "columnsFrom": [ + "user_id_1" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_tent_user_id_2_users_id_fk": { + "name": "user_tent_user_id_2_users_id_fk", + "tableFrom": "user_tent", + "tableTo": "users", + "columnsFrom": [ + "user_id_2" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_tent_user_id_1_user_id_2_pk": { + "name": "user_tent_user_id_1_user_id_2_pk", + "columns": [ + "user_id_1", + "user_id_2" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vssqcmanswer": { + "name": "vssqcmanswer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "questionid": { + "name": "questionid", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "answer_en": { + "name": "answer_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_correct": { + "name": "is_correct", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "vssqcmanswer_questionid_vssqcmquestion_id_fk": { + "name": "vssqcmanswer_questionid_vssqcmquestion_id_fk", + "tableFrom": "vssqcmanswer", + "tableTo": "vssqcmquestion", + "columnsFrom": [ + "questionid" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.vss_form": { + "name": "vss_form", + "schema": "public", + "values": [ + "pending", + "toretry", + "validated", + "rejected" + ] + }, + "public.question_type": { + "name": "question_type", + "schema": "public", + "values": [ + "single_choice", + "multiple_choice" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/src/database/migrations/meta/_journal.json b/backend/src/database/migrations/meta/_journal.json index 73ad2f7..3424d95 100644 --- a/backend/src/database/migrations/meta/_journal.json +++ b/backend/src/database/migrations/meta/_journal.json @@ -1,195 +1,202 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1743857362115, - "tag": "0000_workable_rafael_vega", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1743868246465, - "tag": "0001_stormy_quicksilver", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1743897931065, - "tag": "0002_luxuriant_lockjaw", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1743979532382, - "tag": "0003_material_lorna_dane", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1744064732443, - "tag": "0004_careless_misty_knight", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1744118020611, - "tag": "0005_needy_darwin", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1744126517418, - "tag": "0006_bright_boomerang", - "breakpoints": true - }, - { - "idx": 7, - "version": "7", - "when": 1744212319949, - "tag": "0007_striped_mulholland_black", - "breakpoints": true - }, - { - "idx": 8, - "version": "7", - "when": 1744215076184, - "tag": "0008_striped_bushwacker", - "breakpoints": true - }, - { - "idx": 9, - "version": "7", - "when": 1744241202940, - "tag": "0009_previous_mystique", - "breakpoints": true - }, - { - "idx": 10, - "version": "7", - "when": 1744241242502, - "tag": "0010_fair_mulholland_black", - "breakpoints": true - }, - { - "idx": 11, - "version": "7", - "when": 1744241259194, - "tag": "0011_fearless_nextwave", - "breakpoints": true - }, - { - "idx": 12, - "version": "7", - "when": 1746040673007, - "tag": "0012_productive_giant_man", - "breakpoints": true - }, - { - "idx": 13, - "version": "7", - "when": 1752757642698, - "tag": "0013_curly_angel", - "breakpoints": true - }, - { - "idx": 14, - "version": "7", - "when": 1753638076374, - "tag": "0014_special_reaper", - "breakpoints": true - }, - { - "idx": 15, - "version": "7", - "when": 1753744481956, - "tag": "0015_greedy_genesis", - "breakpoints": true - }, - { - "idx": 16, - "version": "7", - "when": 1754903172897, - "tag": "0016_sudden_ultimatum", - "breakpoints": true - }, - { - "idx": 17, - "version": "7", - "when": 1755637642205, - "tag": "0017_melted_meggan", - "breakpoints": true - }, - { - "idx": 18, - "version": "7", - "when": 1755858317109, - "tag": "0018_puzzling_arachne", - "breakpoints": true - }, - { - "idx": 19, - "version": "7", - "when": 1755906116757, - "tag": "0019_complete_moondragon", - "breakpoints": true - }, - { - "idx": 20, - "version": "7", - "when": 1755907709198, - "tag": "0020_strange_colonel_america", - "breakpoints": true - }, - { - "idx": 21, - "version": "7", - "when": 1756063134903, - "tag": "0021_colossal_madame_web", - "breakpoints": true - }, - { - "idx": 22, - "version": "7", - "when": 1757002717384, - "tag": "0022_light_omega_red", - "breakpoints": true - }, - { - "idx": 23, - "version": "7", - "when": 1782943789540, - "tag": "0023_zippy_colonel_america", - "breakpoints": true - }, - { - "idx": 24, - "version": "7", - "when": 1783776285772, - "tag": "0024_optimal_garia", - "breakpoints": true - }, - { - "idx": 25, - "version": "7", - "when": 1784742510283, - "tag": "0025_perfect_ulik", - "breakpoints": true - }, - { - "idx": 26, - "version": "7", - "when": 1784824629918, - "tag": "0026_swift_excalibur", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1743857362115, + "tag": "0000_workable_rafael_vega", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1743868246465, + "tag": "0001_stormy_quicksilver", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1743897931065, + "tag": "0002_luxuriant_lockjaw", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1743979532382, + "tag": "0003_material_lorna_dane", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1744064732443, + "tag": "0004_careless_misty_knight", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1744118020611, + "tag": "0005_needy_darwin", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1744126517418, + "tag": "0006_bright_boomerang", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1744212319949, + "tag": "0007_striped_mulholland_black", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1744215076184, + "tag": "0008_striped_bushwacker", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1744241202940, + "tag": "0009_previous_mystique", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1744241242502, + "tag": "0010_fair_mulholland_black", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1744241259194, + "tag": "0011_fearless_nextwave", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1746040673007, + "tag": "0012_productive_giant_man", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1752757642698, + "tag": "0013_curly_angel", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1753638076374, + "tag": "0014_special_reaper", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1753744481956, + "tag": "0015_greedy_genesis", + "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1754903172897, + "tag": "0016_sudden_ultimatum", + "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1755637642205, + "tag": "0017_melted_meggan", + "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1755858317109, + "tag": "0018_puzzling_arachne", + "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1755906116757, + "tag": "0019_complete_moondragon", + "breakpoints": true + }, + { + "idx": 20, + "version": "7", + "when": 1755907709198, + "tag": "0020_strange_colonel_america", + "breakpoints": true + }, + { + "idx": 21, + "version": "7", + "when": 1756063134903, + "tag": "0021_colossal_madame_web", + "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1757002717384, + "tag": "0022_light_omega_red", + "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1782943789540, + "tag": "0023_zippy_colonel_america", + "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1783776285772, + "tag": "0024_optimal_garia", + "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1784742510283, + "tag": "0025_perfect_ulik", + "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1784824629918, + "tag": "0026_swift_excalibur", + "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1785000000000, + "tag": "0027_faulty_hellion", + "breakpoints": true + } + ] +} diff --git a/backend/src/schemas/Basic/vssqcmquestion.schema.ts b/backend/src/schemas/Basic/vssqcmquestion.schema.ts index cc9c3c1..fb3e5d0 100644 --- a/backend/src/schemas/Basic/vssqcmquestion.schema.ts +++ b/backend/src/schemas/Basic/vssqcmquestion.schema.ts @@ -5,6 +5,7 @@ export const questionTypeEnum = pgEnum('question_type', ['single_choice', 'multi export const vssqcmquestionSchema = pgTable('vssqcmquestion', { id: serial('id').primaryKey(), question: text('question').notNull(), + question_en: text('question_en'), points: integer('points').notNull(), type: questionTypeEnum('type').notNull(), }); diff --git a/backend/src/schemas/Relational/vssqcmanswer.schema.ts b/backend/src/schemas/Relational/vssqcmanswer.schema.ts index 008c14b..07167dd 100644 --- a/backend/src/schemas/Relational/vssqcmanswer.schema.ts +++ b/backend/src/schemas/Relational/vssqcmanswer.schema.ts @@ -7,6 +7,7 @@ export const vssqcmanswerSchema = pgTable('vssqcmanswer', { .references(() => vssqcmquestionSchema.id, { onDelete: 'cascade' }) .notNull(), answer: text('answer').notNull(), + answer_en: text('answer_en'), is_correct: boolean('is_correct').notNull(), }); diff --git a/backend/src/services/user.service.ts b/backend/src/services/user.service.ts index ff9b817..6737f05 100644 --- a/backend/src/services/user.service.ts +++ b/backend/src/services/user.service.ts @@ -327,18 +327,25 @@ export const getVssQuestionnaire = async () => { const questions = await db.select().from(vssqcmquestionSchema); const answers = await db.select().from(vssqcmanswerSchema); - return questions.map((question) => ({ - id: question.id, - question: question.question, - points: question.points, - type: question.type, - answers: answers - .filter((answer) => answer.questionid === question.id) - .map((answer) => ({ - id: answer.id, - answer: answer.answer, - })), - })); + return questions + .slice() + .sort((firstQuestion, secondQuestion) => firstQuestion.id - secondQuestion.id) + .map((question) => ({ + id: question.id, + question: question.question, + questionEn: question.question_en ?? undefined, + points: question.points, + type: question.type, + answers: answers + .slice() + .sort((firstAnswer, secondAnswer) => firstAnswer.id - secondAnswer.id) + .filter((answer) => answer.questionid === question.id) + .map((answer) => ({ + id: answer.id, + answer: answer.answer, + answerEn: answer.answer_en ?? undefined, + })), + })); } catch (err) { console.error('Erreur lors de la récupération du questionnaire VSS:', err); throw new Error('Erreur de base de données'); diff --git a/backend/src/types/user.d.ts b/backend/src/types/user.d.ts index 4132e01..c4ad664 100644 --- a/backend/src/types/user.d.ts +++ b/backend/src/types/user.d.ts @@ -1,11 +1,13 @@ export type VssQuestionnaireAnswer = { id: number; answer: string; + answerEn?: string; }; export type VssQuestionnaireQuestion = { id: number; question: string; + questionEn?: string; points: number; type: 'single_choice' | 'multiple_choice'; answers: VssQuestionnaireAnswer[]; diff --git a/frontend/src/components/home/emergencyModal.tsx b/frontend/src/components/home/emergencyModal.tsx index a9465f1..5413d05 100644 --- a/frontend/src/components/home/emergencyModal.tsx +++ b/frontend/src/components/home/emergencyModal.tsx @@ -6,15 +6,61 @@ import { createUserContactInformation, getCurrentUserOnboardingStatus } from '.. import { Button } from '../ui/button'; import { Input } from '../ui/input'; import Modal from '../ui/modal'; -import VssModal from './vssModal'; +import VssModal, { type Language } from './vssModal'; type FlowStep = 'idle' | 'loading' | 'emergency' | 'vss'; +const copy = { + fr: { + title: 'Formulaire VSS et Urgence', + toggleLanguage: 'Show in English', + intro: [ + "Bienvenue sur le site de l'intégration !", + "Nous sommes ravis de t'accueillir parmi nous à l'UTT.", + "Durant ta première semaine à l'UTT, tu pourras participer aux activités d'intégration. Afin que celle-ci se déroule dans les meilleures conditions, nous avons besoin que tu répondes à deux formulaires.", + "Dans ce premier formulaire, nous te demandons simplement de renseigner un contact d'urgence, au cas où le moindre problème surviendrait durant cette semaine.", + 'Tu peux quitter ce formulaire à tout moment et le compléter plus tard. Cependant, il est obligatoire pour participer à certaines activités.', + ], + placeholders: { + name: "Nom du contact d'urgence", + phone: "Téléphone du contact d'urgence", + }, + buttons: { + cancel: 'Annuler', + submit: 'Soumettre', + }, + statusError: 'Impossible de récupérer le statut du formulaire.', + submitError: "Impossible d'enregistrer les informations d'urgence.", + }, + en: { + title: 'VSS and Emergency Form', + toggleLanguage: 'Voir en français', + intro: [ + 'Welcome to the integration website!', + 'We are happy to have you with us at UTT.', + 'During your first week at UTT, you will be able to take part in the integration activities. To make sure everything goes as smoothly as possible, we need you to answer two forms.', + 'In this first form, we simply ask you to provide an emergency contact in case the slightest issue happens during this week.', + 'You can leave this form at any time and complete it later. However, it is mandatory to participate in certain activities.', + ], + placeholders: { + name: 'Emergency contact name', + phone: 'Emergency contact phone number', + }, + buttons: { + cancel: 'Cancel', + submit: 'Submit', + }, + statusError: 'Unable to fetch the form status.', + submitError: 'Unable to save the emergency contact information.', + }, +} as const; + function EmergencyModal() { const [searchParams, setSearchParams] = useSearchParams(); const [form, setForm] = useState({ emergency_contact_name: '', emergency_contact_phone: '' }); const [flowStep, setFlowStep] = useState('idle'); const [error, setError] = useState(null); + const [language, setLanguage] = useState('fr'); const token = getToken(); const decodedToken = useMemo(() => (token ? decodeToken(token) : null), [token]); const userPermission = decodedToken?.userPermission; @@ -31,6 +77,7 @@ function EmergencyModal() { setFlowStep('idle'); setForm({ emergency_contact_name: '', emergency_contact_phone: '' }); setError(null); + setLanguage('fr'); return; } @@ -62,7 +109,7 @@ function EmergencyModal() { } catch { if (!cancelled) { setFlowStep('emergency'); - setError('Impossible de récupérer le statut du formulaire.'); + setError(copy[language].statusError); } } }; @@ -81,6 +128,7 @@ function EmergencyModal() { setFlowStep('idle'); setForm({ emergency_contact_name: '', emergency_contact_phone: '' }); setError(null); + setLanguage('fr'); }; const handleContactSubmit = async () => { @@ -91,53 +139,51 @@ function EmergencyModal() { window.dispatchEvent(new Event('user-onboarding-updated')); setFlowStep('vss'); } catch { - setError("Impossible d'enregistrer les informations d'urgence."); + setError(copy[language].submitError); } }; + const localizedCopy = copy[language]; + return ( <>
-

Bienvenue sur le site de l'intégration !

-

Nous sommes ravis de t'accueillir parmi nous à l'UTT.

-

- Durant ta première semaine à l'UTT, tu pourras participer aux activités d'intégration. Afin que - celle-ci se déroule dans les meilleures conditions, nous avons besoin que tu répondes à deux - formulaires. -

-

- Dans ce premier formulaire, nous te demandons simplement de renseigner un contact d'urgence, au - cas où le moindre problème surviendrait durant cette semaine. -

-

- Tu peux quitter ce formulaire à tout moment et le compléter plus tard. Cependant, il est - obligatoire pour participer à certaines activités. -

+
+ +
+ + {localizedCopy.intro.map((paragraph) => ( +

{paragraph}

+ ))} {error && (

{error}

)} setForm({ ...form, emergency_contact_name: e.target.value })} /> setForm({ ...form, emergency_contact_phone: e.target.value })} />
- +
@@ -145,6 +191,8 @@ function EmergencyModal() { setLanguage((currentLanguage) => (currentLanguage === 'fr' ? 'en' : 'fr'))} onSubmitted={() => { window.dispatchEvent(new Event('user-onboarding-updated')); }} diff --git a/frontend/src/components/home/vssModal.tsx b/frontend/src/components/home/vssModal.tsx index e5d7642..d6f2277 100644 --- a/frontend/src/components/home/vssModal.tsx +++ b/frontend/src/components/home/vssModal.tsx @@ -5,9 +5,76 @@ import { getVssQuestionnaire, submitVssQuestionnaire } from '../../services/requ import { Button } from '../ui/button'; import Modal from '../ui/modal'; +export type Language = 'fr' | 'en'; + +const copy = { + fr: { + title: 'Questionnaire VSS', + toggleLanguage: 'Show in English', + intro: [ + 'Dans ce questionnaire, tu devras répondre aux questions ci-dessous à propos des Violences Sexistes et Sexuelles (VSS).', + "La note est sur 14 et tu disposes de deux essais pour obtenir au moins 7 points. Cette sensibilisation est très importante pour nous afin de nous assurer que l'intégration se déroule dans les meilleures conditions pour tout le monde.", + "Si tu n'arrives pas à obtenir la moyenne après deux tentatives, nous serons malheureusement contraints de te refuser l'accès à la Soirée et au Week-end d'intégration, car ce sont les moments où la majorité des situations de VSS se produisent.", + 'Tu peux quitter ce questionnaire à tout moment et le compléter plus tard. Cependant, il est obligatoire pour participer à certaines activités.', + ], + answerGuidance: { + single: 'Choisis une seule réponse.', + multiple: 'Tu peux sélectionner plusieurs réponses.', + }, + loadingError: 'Impossible de charger le questionnaire VSS.', + submitError: 'Impossible d’envoyer le questionnaire VSS.', + unansweredError: "Réponds à toutes les questions avant d'envoyer le questionnaire.", + emptyState: 'Aucun questionnaire disponible pour le moment.', + progress: (answered: number, total: number) => `${answered}/${total} questions répondues`, + points: (totalPoints: number) => `${totalPoints} points possibles`, + submit: 'Soumettre le questionnaire', + submitting: 'Envoi...', + cancel: 'Annuler', + close: 'Fermer', + status: { + validated: 'Questionnaire validé. Tu peux fermer cette fenêtre.', + toretry: 'Le résultat nécessite une seconde tentative. Tu pourras retenter plus tard.', + rejected: 'Le nombre de tentatives autorisées est atteint.', + pending: '', + }, + }, + en: { + title: 'VSS Questionnaire', + toggleLanguage: 'Voir en français', + intro: [ + 'In this questionnaire, you will need to answer the questions below about sexist and sexual violence (VSS).', + 'The score is out of 14 and you have two tries to get at least 7 points. This awareness step is very important to us so that the integration runs in the best possible conditions for everyone.', + 'If you do not reach the passing score after two attempts, we will unfortunately have to deny you access to the Party and the Integration Weekend, because these are the moments where most VSS situations happen.', + 'You can leave this questionnaire at any time and complete it later. However, it is mandatory to participate in certain activities.', + ], + answerGuidance: { + single: 'Choose one answer only.', + multiple: 'You can select multiple answers.', + }, + loadingError: 'Unable to load the VSS questionnaire.', + submitError: 'Unable to submit the VSS questionnaire.', + unansweredError: 'Answer all the questions before submitting the questionnaire.', + emptyState: 'No questionnaire is available right now.', + progress: (answered: number, total: number) => `${answered}/${total} questions answered`, + points: (totalPoints: number) => `${totalPoints} possible points`, + submit: 'Submit questionnaire', + submitting: 'Submitting...', + cancel: 'Cancel', + close: 'Close', + status: { + validated: 'Questionnaire validated. You can close this window.', + toretry: 'The result requires a second attempt. You will be able to try again later.', + rejected: 'The maximum number of attempts has been reached.', + pending: '', + }, + }, +} as const; + interface VssModalProps { visible: boolean; onCancel: () => void; + language: Language; + onToggleLanguage: () => void; onSubmitted?: (result: VssSubmissionResponse) => void; } @@ -20,20 +87,24 @@ const getAnswerClassName = (selected: boolean) => const VssQuestionBlock = ({ question, + language, selectedAnswerIds, onSelect, }: { question: VssQuestionnaireQuestion; + language: Language; selectedAnswerIds: number[]; onSelect: (questionId: number, answerId: number, type: VssQuestionnaireQuestion['type']) => void; }) => { + const localizedQuestion = language === 'en' && question.questionEn ? question.questionEn : question.question; + return (

Question {question.id}

- {question.question} + {localizedQuestion}

@@ -44,6 +115,7 @@ const VssQuestionBlock = ({
{question.answers.map((answer) => { const isSelected = selectedAnswerIds.includes(answer.id); + const localizedAnswer = language === 'en' && answer.answerEn ? answer.answerEn : answer.answer; return ( ); })}

{question.type === 'single_choice' - ? 'Choisis une seule réponse.' - : 'Tu peux sélectionner plusieurs réponses.'} + ? copy[language].answerGuidance.single + : copy[language].answerGuidance.multiple}

); }; -function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { +function VssModal({ visible, onCancel, onSubmitted, language, onToggleLanguage }: VssModalProps) { const [questions, setQuestions] = useState([]); const [selectedAnswers, setSelectedAnswers] = useState>({}); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); + const localizedCopy = copy[language]; useEffect(() => { if (!visible) { @@ -99,7 +172,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { setSelectedAnswers({}); } catch { if (!cancelled) { - setError('Impossible de charger le questionnaire VSS.'); + setError(localizedCopy.loadingError); } } }; @@ -143,7 +216,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { }); if (unansweredQuestions.length > 0) { - setError("Réponds à toutes les questions avant d'envoyer le questionnaire."); + setError(localizedCopy.unansweredError); return; } @@ -157,7 +230,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { setResult(result); onSubmitted?.(result); } catch { - setError('Impossible d’envoyer le questionnaire VSS.'); + setError(localizedCopy.submitError); } finally { setSubmitting(false); } @@ -166,41 +239,25 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { const answeredCount = questions.filter((question) => (selectedAnswers[question.id] ?? []).length > 0).length; const totalQuestions = questions.length; - const statusMessage = - result?.status === 'validated' - ? 'Questionnaire validé. Tu peux fermer cette fenêtre.' - : result?.status === 'toretry' - ? 'Le résultat nécessite une seconde tentative. Tu pourras retenter plus tard.' - : result?.status === 'rejected' - ? 'Le nombre de tentatives autorisées est atteint.' - : null; + const statusMessage = result ? localizedCopy.status[result.status] : null; return (
-

- Dans ce questionnaire, tu devras répondre aux questions ci-dessous à propos des Violences Sexistes - et Sexuelles (VSS). -

-

- La note est sur 14 et tu disposes de deux essais pour obtenir au moins 7 points. Cette - sensibilisation est très importante pour nous afin de nous assurer que l'intégration se déroule dans - les meilleures conditions pour tout le monde. -

-

- Si tu n'arrives pas à obtenir la moyenne après deux tentatives, nous serons malheureusement - contraints de te refuser l'accès à la Soirée et au Week-end d'intégration, car ce sont les moments - où la majorité des situations de VSS se produisent. -

-

- Tu peux quitter ce questionnaire à tout moment et le compléter plus tard. Cependant, il est - obligatoire pour participer à certaines activités. -

+
+ +
+ + {localizedCopy.intro.map((paragraph) => ( +

{paragraph}

+ ))} {error && (
@@ -229,11 +286,11 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { {questions.length > 0 && (
+ {localizedCopy.progress(answeredCount, totalQuestions)} - {answeredCount}/{totalQuestions} questions répondues - - - {questions.reduce((total, question) => total + question.points, 0)} points possibles + {localizedCopy.points( + questions.reduce((total, question) => total + question.points, 0), + )}
@@ -241,6 +298,7 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { @@ -250,20 +308,20 @@ function VssModal({ visible, onCancel, onSubmitted }: VssModalProps) { {questions.length === 0 && !error && (
- Aucun questionnaire disponible pour le moment. + {localizedCopy.emptyState}
)}
{result ? ( - + ) : ( <> )} diff --git a/frontend/src/interfaces/user.interface.ts b/frontend/src/interfaces/user.interface.ts index b53abce..5234bc2 100644 --- a/frontend/src/interfaces/user.interface.ts +++ b/frontend/src/interfaces/user.interface.ts @@ -31,11 +31,13 @@ export interface UserOnboardingStatus { export interface VssQuestionnaireAnswer { id: number; answer: string; + answerEn?: string; } export interface VssQuestionnaireQuestion { id: number; question: string; + questionEn?: string; points: number; type: 'single_choice' | 'multiple_choice'; answers: VssQuestionnaireAnswer[];