feat: Ajout de la commande npm permettant de creer un etablissement

This commit is contained in:
Luc SORIGNET
2026-04-04 11:57:59 +02:00
parent 2579af9b8b
commit 09b1541dc8
8 changed files with 450 additions and 8 deletions

View File

@ -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.

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
node_modules/
hardcoded-strings-report.md
backend.env
*.log
*.log
.claude/worktrees/*

3
.husky/commit-msg Normal file → Executable file
View File

@ -1 +1,2 @@
npx --no -- commitlint --edit $1
#!/bin/sh
npx --no -- commitlint --edit $1

3
.husky/pre-commit Normal file → Executable file
View File

@ -1 +1,2 @@
cd $(dirname "$0")/../Front-End/ && npm run lint-light
#!/bin/sh
cd $(dirname "$0")/../Front-End/ && npm run lint-light

3
.husky/prepare-commit-msg Normal file → Executable file
View File

@ -1 +1,2 @@
#node scripts/prepare-commit-msg.js "$1" "$2"
#!/bin/sh
#node scripts/prepare-commit-msg.js "$1" "$2"

View File

@ -51,4 +51,4 @@
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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 <chemin/vers/batch.json>",
);
process.exit(1);
}
await runBatch(filePath);
} else {
await runInteractive();
}
}
main().catch((err) => {
console.error("❌ Erreur inattendue:", err.message);
process.exit(1);
});