From 09b1541dc83b28dcba0aead633755d58e9a534fa Mon Sep 17 00:00:00 2001 From: Luc SORIGNET Date: Sat, 4 Apr 2026 11:57:59 +0200 Subject: [PATCH] feat: Ajout de la commande npm permettant de creer un etablissement --- .../instructions/documentation.instruction.md | 2 +- .gitignore | 3 +- .husky/commit-msg | 3 +- .husky/pre-commit | 3 +- .husky/prepare-commit-msg | 3 +- Front-End/package.json | 2 +- package.json | 5 +- scripts/create-establishment.js | 437 ++++++++++++++++++ 8 files changed, 450 insertions(+), 8 deletions(-) mode change 100644 => 100755 .husky/commit-msg mode change 100644 => 100755 .husky/pre-commit mode change 100644 => 100755 .husky/prepare-commit-msg create mode 100644 scripts/create-establishment.js diff --git a/.github/instructions/documentation.instruction.md b/.github/instructions/documentation.instruction.md index cfc35e4..bb8b1fb 100644 --- a/.github/instructions/documentation.instruction.md +++ b/.github/instructions/documentation.instruction.md @@ -1,5 +1,5 @@ La documentation doit être en français et claire pour les utilisateurs francophones. -Toutes la documentation doit être dans le dossier docs/ +Toutes la documentation doit être dans le dossier docs/ à la racine. Seul les fichiers README.md, CHANGELOG.md doivent être à la racine. La documentation doit être conscise et pertinente, sans répétitions inutiles entre les documents. Tout ce qui concerne la gestion de projet, roadmap ne doit pas apparaître dans la documentation. diff --git a/.gitignore b/.gitignore index 3d3a5fd..2dcbe0b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules/ hardcoded-strings-report.md backend.env -*.log \ No newline at end of file +*.log +.claude/worktrees/* \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg old mode 100644 new mode 100755 index 34eed8b..2cd2c32 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1 +1,2 @@ -npx --no -- commitlint --edit $1 \ No newline at end of file +#!/bin/sh +npx --no -- commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 index cc642e9..15f570a --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ -cd $(dirname "$0")/../Front-End/ && npm run lint-light \ No newline at end of file +#!/bin/sh +cd $(dirname "$0")/../Front-End/ && npm run lint-light diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg old mode 100644 new mode 100755 index 7282778..bc3deb1 --- a/.husky/prepare-commit-msg +++ b/.husky/prepare-commit-msg @@ -1 +1,2 @@ -#node scripts/prepare-commit-msg.js "$1" "$2" \ No newline at end of file +#!/bin/sh +#node scripts/prepare-commit-msg.js "$1" "$2" diff --git a/Front-End/package.json b/Front-End/package.json index 335df53..5756fb9 100644 --- a/Front-End/package.json +++ b/Front-End/package.json @@ -51,4 +51,4 @@ "postcss": "^8.4.47", "tailwindcss": "^3.4.14" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 606fc35..2b6ac33 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "prepare": "husky", "release": "standard-version", "update-version": "node scripts/update-version.js", - "format": "prettier --write Front-End" + "format": "prettier --write Front-End", + "create-establishment": "node scripts/create-establishment.js" }, "standard-version": { "scripts": { @@ -19,4 +20,4 @@ "prettier": "^3.5.3", "standard-version": "^9.5.0" } -} +} \ No newline at end of file diff --git a/scripts/create-establishment.js b/scripts/create-establishment.js new file mode 100644 index 0000000..df6c7ee --- /dev/null +++ b/scripts/create-establishment.js @@ -0,0 +1,437 @@ +#!/usr/bin/env node + +/** + * CLI pour créer un ou plusieurs établissements N3WT. + * + * Usage interactif : + * node scripts/create-establishment.js + * + * Usage batch (fichier JSON) : + * node scripts/create-establishment.js --file batch.json + * + * Format du fichier batch : + * { + * "backendUrl": "http://localhost:8080", // optionnel (fallback : URL_DJANGO dans conf/backend.env) + * "apiKey": "TOk3n1234!!", // optionnel (fallback : WEBHOOK_API_KEY dans conf/backend.env) + * "establishments": [ + * { + * "name": "École Dupont", + * "address": "1 rue de la Paix, Paris", + * "total_capacity": 300, + * "establishment_type": [1, 2], // 1=Maternelle, 2=Primaire, 3=Secondaire + * "evaluation_frequency": 1, // 1=Trimestre, 2=Semestre, 3=Année + * "licence_code": "LIC001", // optionnel + * "directeur": { + * "email": "directeur@dupont.fr", + * "password": "motdepasse123", + * "last_name": "Dupont", + * "first_name": "Jean" + * } + * } + * ] + * } + */ + +const readline = require("readline"); +const http = require("http"); +const https = require("https"); +const fs = require("fs"); +const path = require("path"); + +// ── Lecture de conf/backend.env ─────────────────────────────────────────────── + +function loadBackendEnv() { + const envPath = path.join(__dirname, "..", "conf", "backend.env"); + if (!fs.existsSync(envPath)) return; + + const lines = fs.readFileSync(envPath, "utf8").split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let value = trimmed.slice(eqIdx + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + // Ne pas écraser les variables déjà définies dans l'environnement + if (!(key in process.env)) { + process.env[key] = value; + } + } +} + +// ── Helpers readline ────────────────────────────────────────────────────────── + +let rl; + +function getRL() { + if (!rl) { + rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + } + return rl; +} + +function ask(question, defaultValue) { + const suffix = + defaultValue != null && defaultValue !== "" ? ` (${defaultValue})` : ""; + return new Promise((resolve) => { + getRL().question(`${question}${suffix}: `, (answer) => { + resolve( + answer.trim() || (defaultValue != null ? String(defaultValue) : ""), + ); + }); + }); +} + +function askRequired(question) { + return new Promise((resolve) => { + const prompt = () => { + getRL().question(`${question}: `, (answer) => { + if (!answer.trim()) { + console.log(" ⚠ Ce champ est obligatoire."); + prompt(); + } else { + resolve(answer.trim()); + } + }); + }; + prompt(); + }); +} + +function askChoices(question, choices) { + return new Promise((resolve) => { + console.log(`\n${question}`); + choices.forEach((c, i) => console.log(` ${i + 1}. ${c.label}`)); + const prompt = () => { + getRL().question( + "Choix (numéros séparés par des virgules): ", + (answer) => { + const nums = answer + .split(",") + .map((s) => parseInt(s.trim(), 10)) + .filter((n) => n >= 1 && n <= choices.length); + if (nums.length === 0) { + console.log(" ⚠ Sélectionnez au moins une option."); + prompt(); + } else { + resolve(nums.map((n) => choices[n - 1].value)); + } + }, + ); + }; + prompt(); + }); +} + +function askChoice(question, choices, defaultIndex) { + return new Promise((resolve) => { + console.log(`\n${question}`); + choices.forEach((c, i) => + console.log( + ` ${i + 1}. ${c.label}${i === defaultIndex ? " (défaut)" : ""}`, + ), + ); + const prompt = () => { + getRL().question(`Choix (1-${choices.length}): `, (answer) => { + if (!answer.trim() && defaultIndex != null) { + resolve(choices[defaultIndex].value); + return; + } + const n = parseInt(answer.trim(), 10); + if (n >= 1 && n <= choices.length) { + resolve(choices[n - 1].value); + } else { + console.log(" ⚠ Choix invalide."); + prompt(); + } + }); + }; + prompt(); + }); +} + +// ── HTTP ────────────────────────────────────────────────────────────────────── + +function postJSON(url, data, apiKey) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const mod = parsed.protocol === "https:" ? https : http; + const body = JSON.stringify(data); + + const req = mod.request( + { + hostname: parsed.hostname, + port: parsed.port, + path: parsed.pathname, + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + "X-API-Key": apiKey, + }, + }, + (res) => { + let raw = ""; + res.on("data", (chunk) => (raw += chunk)); + res.on("end", () => { + try { + resolve({ status: res.statusCode, data: JSON.parse(raw) }); + } catch { + resolve({ status: res.statusCode, data: raw }); + } + }); + }, + ); + + req.on("error", reject); + req.write(body); + req.end(); + }); +} + +// ── Création d'un établissement ─────────────────────────────────────────────── + +async function createEstablishment(backendUrl, apiKey, payload) { + const url = `${backendUrl.replace(/\/$/, "")}/Establishment/establishments`; + const res = await postJSON(url, payload, apiKey); + return res; +} + +// ── Validation basique d'un enregistrement batch ────────────────────────────── + +function validateRecord(record, index) { + const errors = []; + const label = record.name ? `"${record.name}"` : `#${index + 1}`; + + if (!record.name) errors.push("name manquant"); + if (!record.address) errors.push("address manquant"); + if (!record.total_capacity) errors.push("total_capacity manquant"); + if ( + !Array.isArray(record.establishment_type) || + record.establishment_type.length === 0 + ) + errors.push("establishment_type manquant ou vide"); + if (!record.directeur) errors.push("directeur manquant"); + else { + if (!record.directeur.email) errors.push("directeur.email manquant"); + if (!record.directeur.password) errors.push("directeur.password manquant"); + if (!record.directeur.last_name) + errors.push("directeur.last_name manquant"); + if (!record.directeur.first_name) + errors.push("directeur.first_name manquant"); + } + + if (errors.length > 0) { + throw new Error(`Établissement ${label} invalide : ${errors.join(", ")}`); + } +} + +// ── Mode batch ──────────────────────────────────────────────────────────────── + +async function runBatch(filePath) { + const absPath = path.resolve(filePath); + if (!fs.existsSync(absPath)) { + console.error(`❌ Fichier introuvable : ${absPath}`); + process.exit(1); + } + + let batch; + try { + batch = JSON.parse(fs.readFileSync(absPath, "utf8")); + } catch (err) { + console.error(`❌ Fichier JSON invalide : ${err.message}`); + process.exit(1); + } + + const backendUrl = + batch.backendUrl || process.env.URL_DJANGO || "http://localhost:8080"; + const apiKey = batch.apiKey || process.env.WEBHOOK_API_KEY; + + if (!apiKey) { + console.error( + "❌ apiKey manquant dans le fichier batch ou la variable WEBHOOK_API_KEY.", + ); + process.exit(1); + } + + const establishments = batch.establishments; + if (!Array.isArray(establishments) || establishments.length === 0) { + console.error("❌ Le fichier batch ne contient aucun établissement."); + process.exit(1); + } + + // Validation préalable de tous les enregistrements + establishments.forEach((record, i) => validateRecord(record, i)); + + console.log( + `\n📋 Batch : ${establishments.length} établissement(s) à créer sur ${backendUrl}\n`, + ); + + let success = 0; + let failure = 0; + + for (let i = 0; i < establishments.length; i++) { + const record = establishments[i]; + const label = `[${i + 1}/${establishments.length}] ${record.name}`; + process.stdout.write(` ${label} ... `); + + try { + const res = await createEstablishment(backendUrl, apiKey, record); + if (res.status === 201) { + console.log(`✅ (ID: ${res.data.id})`); + success++; + } else { + console.log(`❌ HTTP ${res.status}`); + console.error(` ${JSON.stringify(res.data)}`); + failure++; + } + } catch (err) { + console.log(`❌ Erreur réseau : ${err.message}`); + failure++; + } + } + + console.log(`\nRésultat : ${success} créé(s), ${failure} échec(s).`); + if (failure > 0) process.exit(1); +} + +// ── Mode interactif ─────────────────────────────────────────────────────────── + +async function runInteractive() { + console.log("╔══════════════════════════════════════════╗"); + console.log("║ Création d'un établissement N3WT ║"); + console.log("╚══════════════════════════════════════════╝\n"); + + const backendUrl = await ask( + "URL du backend Django", + process.env.URL_DJANGO || "http://localhost:8080", + ); + const apiKey = await ask( + "Clé API webhook (WEBHOOK_API_KEY)", + process.env.WEBHOOK_API_KEY || "", + ); + if (!apiKey) { + console.error("❌ La clé API est obligatoire."); + getRL().close(); + process.exit(1); + } + + // --- Établissement --- + console.log("\n── Établissement ──"); + const name = await askRequired("Nom de l'établissement"); + const address = await askRequired("Adresse"); + const totalCapacity = parseInt(await askRequired("Capacité totale"), 10); + + const establishmentType = await askChoices("Type(s) de structure:", [ + { label: "Maternelle", value: 1 }, + { label: "Primaire", value: 2 }, + { label: "Secondaire", value: 3 }, + ]); + + const evaluationFrequency = await askChoice( + "Fréquence d'évaluation:", + [ + { label: "Trimestre", value: 1 }, + { label: "Semestre", value: 2 }, + { label: "Année", value: 3 }, + ], + 0, + ); + + const licenceCode = await ask("Code licence (optionnel)", ""); + + // --- Directeur (admin) --- + console.log("\n── Compte directeur (admin) ──"); + const directorEmail = await askRequired("Email du directeur"); + const directorPassword = await askRequired("Mot de passe"); + const directorLastName = await askRequired("Nom de famille"); + const directorFirstName = await askRequired("Prénom"); + + // --- Récapitulatif --- + console.log("\n── Récapitulatif ──"); + console.log(` Établissement : ${name}`); + console.log(` Adresse : ${address}`); + console.log(` Capacité : ${totalCapacity}`); + console.log(` Type(s) : ${establishmentType.join(", ")}`); + console.log(` Évaluation : ${evaluationFrequency}`); + console.log( + ` Directeur : ${directorFirstName} ${directorLastName} <${directorEmail}>`, + ); + + const confirm = await ask("\nConfirmer la création ? (o/n)", "o"); + if (confirm.toLowerCase() !== "o") { + console.log("Annulé."); + getRL().close(); + process.exit(0); + } + + const payload = { + name, + address, + total_capacity: totalCapacity, + establishment_type: establishmentType, + evaluation_frequency: evaluationFrequency, + ...(licenceCode && { licence_code: licenceCode }), + directeur: { + email: directorEmail, + password: directorPassword, + last_name: directorLastName, + first_name: directorFirstName, + }, + }; + + console.log("\nCréation en cours..."); + try { + const res = await createEstablishment(backendUrl, apiKey, payload); + if (res.status === 201) { + console.log("\n✅ Établissement créé avec succès !"); + console.log(` ID : ${res.data.id}`); + console.log(` Nom : ${res.data.name}`); + } else { + console.error(`\n❌ Erreur (HTTP ${res.status}):`); + console.error(JSON.stringify(res.data, null, 2)); + process.exit(1); + } + } catch (err) { + console.error("\n❌ Erreur réseau:", err.message); + process.exit(1); + } + + getRL().close(); +} + +// ── Point d'entrée ──────────────────────────────────────────────────────────── + +async function main() { + loadBackendEnv(); + + const args = process.argv.slice(2); + const fileIndex = args.indexOf("--file"); + + if (fileIndex !== -1) { + const filePath = args[fileIndex + 1]; + if (!filePath) { + console.error( + "❌ Argument --file manquant. Usage : --file ", + ); + process.exit(1); + } + await runBatch(filePath); + } else { + await runInteractive(); + } +} + +main().catch((err) => { + console.error("❌ Erreur inattendue:", err.message); + process.exit(1); +});