deploy: equitask — 2026-04-28 19:51:14

This commit is contained in:
deploy.py
2026-04-28 19:51:14 +02:00
commit 53532f7f59
47 changed files with 4093 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "equitask-backend",
"version": "1.0.0",
"description": "Backend Express + SQLite pour EquiTask",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/db/migrate.ts",
"db:seed": "tsx src/db/seed.ts"
},
"dependencies": {
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"drizzle-orm": "^0.30.10",
"express": "^4.18.3",
"helmet": "^7.1.0",
"morgan": "^1.10.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.8",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/node": "^20.12.2",
"drizzle-kit": "^0.21.4",
"tsx": "^4.7.2",
"typescript": "^5.4.5"
}
}
+94
View File
@@ -0,0 +1,94 @@
// src/db/index.ts
// Connexion à la base de données SQLite via Drizzle ORM
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import * as schema from './schema';
import path from 'path';
import fs from 'fs';
// Chemin de la base de données - configurable via variable d'environnement
const DB_PATH = process.env.DATABASE_PATH || path.join(process.cwd(), 'data', 'equitask.db');
// Créer le répertoire si nécessaire
const dbDir = path.dirname(DB_PATH);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
// Connexion SQLite
const sqlite = new Database(DB_PATH);
// Activer WAL mode pour de meilleures performances
sqlite.pragma('journal_mode = WAL');
sqlite.pragma('foreign_keys = ON');
// Instance Drizzle
export const db = drizzle(sqlite, { schema });
// Initialiser les tables si elles n'existent pas
export function initDb() {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS foyer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
cree_le TEXT DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS membres (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('adulte', 'enfant')),
couleur TEXT NOT NULL,
actif INTEGER DEFAULT 1,
ordre INTEGER DEFAULT 0,
cree_le TEXT DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
icone TEXT NOT NULL,
couleur TEXT NOT NULL,
ordre INTEGER DEFAULT 0,
actif INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS taches_recurrentes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
categorie_id INTEGER NOT NULL,
duree_moyenne_min INTEGER NOT NULL,
coefficient_penibilite INTEGER NOT NULL CHECK(coefficient_penibilite BETWEEN 1 AND 5),
actif INTEGER DEFAULT 1,
FOREIGN KEY (categorie_id) REFERENCES categories(id)
);
CREATE TABLE IF NOT EXISTS saisies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tache_recurrente_id INTEGER,
nom_tache_oneshot TEXT,
categorie_id INTEGER NOT NULL,
membre_id INTEGER NOT NULL,
date_heure TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
duree_reelle_min INTEGER,
coefficient_penibilite INTEGER NOT NULL CHECK(coefficient_penibilite BETWEEN 1 AND 5),
score_final INTEGER NOT NULL,
notes TEXT,
synced INTEGER DEFAULT 1,
cree_le TEXT DEFAULT (datetime('now', 'localtime')),
FOREIGN KEY (tache_recurrente_id) REFERENCES taches_recurrentes(id),
FOREIGN KEY (categorie_id) REFERENCES categories(id),
FOREIGN KEY (membre_id) REFERENCES membres(id),
CHECK (tache_recurrente_id IS NOT NULL OR nom_tache_oneshot IS NOT NULL)
);
CREATE INDEX IF NOT EXISTS idx_saisies_membre ON saisies(membre_id);
CREATE INDEX IF NOT EXISTS idx_saisies_date ON saisies(date_heure);
CREATE INDEX IF NOT EXISTS idx_saisies_categorie ON saisies(categorie_id);
`);
console.log('✅ Base de données initialisée :', DB_PATH);
}
export { sqlite };
+72
View File
@@ -0,0 +1,72 @@
// src/db/schema.ts
// Schéma de la base de données EquiTask - Drizzle ORM + SQLite
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
/** Table foyer - un seul enregistrement par instance */
export const foyer = sqliteTable('foyer', {
id: integer('id').primaryKey({ autoIncrement: true }),
nom: text('nom').notNull(),
cree_le: text('cree_le').default(sql`(datetime('now', 'localtime'))`),
});
/** Table membres - jusqu'à 7 membres par foyer */
export const membres = sqliteTable('membres', {
id: integer('id').primaryKey({ autoIncrement: true }),
nom: text('nom').notNull(),
role: text('role').notNull(), // 'adulte' | 'enfant'
couleur: text('couleur').notNull(), // hex color ex: #ef4444
actif: integer('actif').default(1), // 1 = actif, 0 = inactif
ordre: integer('ordre').default(0), // ordre d'affichage
cree_le: text('cree_le').default(sql`(datetime('now', 'localtime'))`),
});
/** Table categories - catégories de tâches, éditables */
export const categories = sqliteTable('categories', {
id: integer('id').primaryKey({ autoIncrement: true }),
nom: text('nom').notNull(),
icone: text('icone').notNull(), // emoji
couleur: text('couleur').notNull(), // hex color
ordre: integer('ordre').default(0),
actif: integer('actif').default(1),
});
/** Table taches_recurrentes - catalogue des tâches habituelles */
export const tachesRecurrentes = sqliteTable('taches_recurrentes', {
id: integer('id').primaryKey({ autoIncrement: true }),
nom: text('nom').notNull(),
categorie_id: integer('categorie_id').notNull().references(() => categories.id),
duree_moyenne_min: integer('duree_moyenne_min').notNull(), // durée en minutes
coefficient_penibilite: integer('coefficient_penibilite').notNull(), // 1 à 5
// score_calcule = duree_moyenne_min * coefficient_penibilite (calculé côté serveur)
actif: integer('actif').default(1),
});
/** Table saisies - historique des tâches effectuées */
export const saisies = sqliteTable('saisies', {
id: integer('id').primaryKey({ autoIncrement: true }),
tache_recurrente_id: integer('tache_recurrente_id').references(() => tachesRecurrentes.id),
nom_tache_oneshot: text('nom_tache_oneshot'), // nullable - tâche ponctuelle
categorie_id: integer('categorie_id').notNull().references(() => categories.id),
membre_id: integer('membre_id').notNull().references(() => membres.id),
date_heure: text('date_heure').notNull().default(sql`(datetime('now', 'localtime'))`),
duree_reelle_min: integer('duree_reelle_min'), // nullable - override de la durée moyenne
coefficient_penibilite: integer('coefficient_penibilite').notNull(),
score_final: integer('score_final').notNull(), // duree * coef, stocké en snapshot
notes: text('notes'), // nullable
synced: integer('synced').default(1), // flag pour sync offline
cree_le: text('cree_le').default(sql`(datetime('now', 'localtime'))`),
});
// Types TypeScript exportés
export type Foyer = typeof foyer.$inferSelect;
export type NouveauFoyer = typeof foyer.$inferInsert;
export type Membre = typeof membres.$inferSelect;
export type NouveauMembre = typeof membres.$inferInsert;
export type Categorie = typeof categories.$inferSelect;
export type NouvelleCategorie = typeof categories.$inferInsert;
export type TacheRecurrente = typeof tachesRecurrentes.$inferSelect;
export type NouvelleTache = typeof tachesRecurrentes.$inferInsert;
export type Saisie = typeof saisies.$inferSelect;
export type NouvelleSaisie = typeof saisies.$inferInsert;
+71
View File
@@ -0,0 +1,71 @@
// src/db/seed-sync.ts
// Seed synchrone exécuté au premier démarrage
import { db } from './index';
import { categories, tachesRecurrentes } from './schema';
const CATEGORIES_DEFAUT = [
{ nom: 'Cuisine', icone: '🍳', couleur: '#ef4444', ordre: 1 },
{ nom: 'Ménage', icone: '🧹', couleur: '#f97316', ordre: 2 },
{ nom: 'Courses', icone: '🛒', couleur: '#eab308', ordre: 3 },
{ nom: 'Enfants', icone: '👶', couleur: '#22c55e', ordre: 4 },
{ nom: 'Administratif', icone: '📋', couleur: '#3b82f6', ordre: 5 },
{ nom: 'Entretien maison', icone: '🔧', couleur: '#8b5cf6', ordre: 6 },
{ nom: 'Charge mentale', icone: '🧠', couleur: '#ec4899', ordre: 7 },
];
const TACHES_DEFAUT = [
{ nom: 'Préparation repas', cat: 'Cuisine', duree: 45, coef: 3 },
{ nom: 'Vaisselle / Plonge', cat: 'Cuisine', duree: 20, coef: 2 },
{ nom: 'Rangement cuisine', cat: 'Cuisine', duree: 15, coef: 1 },
{ nom: 'Batch cooking semaine', cat: 'Cuisine', duree: 90, coef: 3 },
{ nom: 'Petit-déjeuner / Goûter enfants', cat: 'Cuisine', duree: 20, coef: 2 },
{ nom: 'Aspiration / Balayage sols', cat: 'Ménage', duree: 30, coef: 2 },
{ nom: 'Nettoyage salle de bain', cat: 'Ménage', duree: 25, coef: 3 },
{ nom: 'Nettoyage WC', cat: 'Ménage', duree: 10, coef: 4 },
{ nom: 'Lessive (tri + machine)', cat: 'Ménage', duree: 20, coef: 2 },
{ nom: 'Étendage / Rangement linge', cat: 'Ménage', duree: 25, coef: 2 },
{ nom: 'Repassage', cat: 'Ménage', duree: 45, coef: 3 },
{ nom: 'Sortie des poubelles', cat: 'Ménage', duree: 10, coef: 2 },
{ nom: 'Courses alimentaires', cat: 'Courses', duree: 60, coef: 2 },
{ nom: 'Gestion liste de courses', cat: 'Courses', duree: 15, coef: 2 },
{ nom: 'Commandes en ligne', cat: 'Courses', duree: 20, coef: 2 },
{ nom: 'Bain / Douche enfants', cat: 'Enfants', duree: 30, coef: 3 },
{ nom: 'Aide aux devoirs', cat: 'Enfants', duree: 45, coef: 3 },
{ nom: 'Conduite activités extrascolaires', cat: 'Enfants', duree: 60, coef: 2 },
{ nom: 'Préparation cartable / affaires', cat: 'Enfants', duree: 15, coef: 2 },
{ nom: 'Planifier RDV médicaux', cat: 'Administratif', duree: 20, coef: 3 },
{ nom: 'Gestion papiers / courrier', cat: 'Administratif', duree: 30, coef: 3 },
{ nom: 'Déclarations / impôts', cat: 'Administratif', duree: 60, coef: 4 },
{ nom: 'Suivi factures / finances', cat: 'Administratif', duree: 30, coef: 3 },
{ nom: 'Bricolage / Réparations', cat: 'Entretien maison', duree: 60, coef: 3 },
{ nom: 'Jardinage / Balcon', cat: 'Entretien maison', duree: 60, coef: 2 },
{ nom: 'Nettoyage vitres', cat: 'Entretien maison', duree: 45, coef: 3 },
{ nom: 'Planification semaine famille', cat: 'Charge mentale', duree: 30, coef: 4 },
{ nom: 'Suivi scolarité enfants', cat: 'Charge mentale', duree: 20, coef: 3 },
{ nom: 'Gestion santé famille', cat: 'Charge mentale', duree: 30, coef: 4 },
{ nom: 'Organisation vacances / sorties', cat: 'Charge mentale', duree: 45, coef: 3 },
];
// Insérer catégories
for (const cat of CATEGORIES_DEFAUT) {
db.insert(categories).values(cat).run();
}
const catsInserted = db.select().from(categories).all();
const catMap = new Map(catsInserted.map(c => [c.nom, c.id]));
// Insérer tâches
for (const tache of TACHES_DEFAUT) {
const catId = catMap.get(tache.cat);
if (catId) {
db.insert(tachesRecurrentes).values({
nom: tache.nom,
categorie_id: catId,
duree_moyenne_min: tache.duree,
coefficient_penibilite: tache.coef,
}).run();
}
}
console.log('✅ Seed synchrone terminé');
+106
View File
@@ -0,0 +1,106 @@
// src/db/seed.ts
// Données initiales : 7 catégories par défaut + tâches récurrentes courantes
import { db, initDb } from './index';
import { categories, tachesRecurrentes, foyer } from './schema';
import { eq } from 'drizzle-orm';
/** Catégories par défaut avec emojis et couleurs */
const CATEGORIES_DEFAUT = [
{ nom: 'Cuisine', icone: '🍳', couleur: '#ef4444', ordre: 1 },
{ nom: 'Ménage', icone: '🧹', couleur: '#f97316', ordre: 2 },
{ nom: 'Courses', icone: '🛒', couleur: '#eab308', ordre: 3 },
{ nom: 'Enfants', icone: '👶', couleur: '#22c55e', ordre: 4 },
{ nom: 'Administratif', icone: '📋', couleur: '#3b82f6', ordre: 5 },
{ nom: 'Entretien maison', icone: '🔧', couleur: '#8b5cf6', ordre: 6 },
{ nom: 'Charge mentale', icone: '🧠', couleur: '#ec4899', ordre: 7 },
];
/** Tâches récurrentes par catégorie (durée en min, pénibilité 1-5) */
const TACHES_DEFAUT: { nom: string; cat: string; duree: number; coef: number }[] = [
// Cuisine
{ nom: 'Préparation repas', cat: 'Cuisine', duree: 45, coef: 3 },
{ nom: 'Vaisselle / Plonge', cat: 'Cuisine', duree: 20, coef: 2 },
{ nom: 'Rangement cuisine', cat: 'Cuisine', duree: 15, coef: 1 },
{ nom: 'Batch cooking semaine', cat: 'Cuisine', duree: 90, coef: 3 },
{ nom: 'Petit-déjeuner / Goûter enfants', cat: 'Cuisine', duree: 20, coef: 2 },
// Ménage
{ nom: 'Aspiration / Balayage sols', cat: 'Ménage', duree: 30, coef: 2 },
{ nom: 'Nettoyage salle de bain', cat: 'Ménage', duree: 25, coef: 3 },
{ nom: 'Nettoyage WC', cat: 'Ménage', duree: 10, coef: 4 },
{ nom: 'Lessive (tri + machine)', cat: 'Ménage', duree: 20, coef: 2 },
{ nom: 'Étendage / Rangement linge', cat: 'Ménage', duree: 25, coef: 2 },
{ nom: 'Repassage', cat: 'Ménage', duree: 45, coef: 3 },
{ nom: 'Sortie des poubelles', cat: 'Ménage', duree: 10, coef: 2 },
// Courses
{ nom: 'Courses alimentaires', cat: 'Courses', duree: 60, coef: 2 },
{ nom: 'Gestion liste de courses', cat: 'Courses', duree: 15, coef: 2 },
{ nom: 'Commandes en ligne', cat: 'Courses', duree: 20, coef: 2 },
// Enfants
{ nom: 'Bain / Douche enfants', cat: 'Enfants', duree: 30, coef: 3 },
{ nom: 'Aide aux devoirs', cat: 'Enfants', duree: 45, coef: 3 },
{ nom: 'Conduite activités extrascolaires', cat: 'Enfants', duree: 60, coef: 2 },
{ nom: 'Préparation cartable / affaires', cat: 'Enfants', duree: 15, coef: 2 },
// Administratif
{ nom: 'Planifier RDV médicaux', cat: 'Administratif', duree: 20, coef: 3 },
{ nom: 'Gestion papiers / courrier', cat: 'Administratif', duree: 30, coef: 3 },
{ nom: 'Déclarations / impôts', cat: 'Administratif', duree: 60, coef: 4 },
{ nom: 'Suivi factures / finances', cat: 'Administratif', duree: 30, coef: 3 },
// Entretien maison
{ nom: 'Bricolage / Réparations', cat: 'Entretien maison', duree: 60, coef: 3 },
{ nom: 'Jardinage / Balcon', cat: 'Entretien maison', duree: 60, coef: 2 },
{ nom: 'Nettoyage vitres', cat: 'Entretien maison', duree: 45, coef: 3 },
// Charge mentale
{ nom: 'Planification semaine famille', cat: 'Charge mentale', duree: 30, coef: 4 },
{ nom: 'Suivi scolarité enfants', cat: 'Charge mentale', duree: 20, coef: 3 },
{ nom: 'Gestion santé famille', cat: 'Charge mentale', duree: 30, coef: 4 },
{ nom: 'Organisation vacances / sorties', cat: 'Charge mentale', duree: 45, coef: 3 },
];
async function seed() {
console.log('🌱 Démarrage du seed...');
initDb();
// Vérifier si les catégories existent déjà
const existantes = db.select().from(categories).all();
if (existantes.length > 0) {
console.log('️ Données déjà présentes, seed ignoré.');
return;
}
// Insérer les catégories
console.log('📂 Insertion des catégories...');
for (const cat of CATEGORIES_DEFAUT) {
db.insert(categories).values(cat).run();
}
// Récupérer les catégories insérées pour obtenir leurs IDs
const catsInserted = db.select().from(categories).all();
const catMap = new Map(catsInserted.map(c => [c.nom, c.id]));
// Insérer les tâches récurrentes
console.log('📋 Insertion des tâches récurrentes...');
for (const tache of TACHES_DEFAUT) {
const catId = catMap.get(tache.cat);
if (!catId) {
console.warn(`⚠️ Catégorie "${tache.cat}" introuvable pour la tâche "${tache.nom}"`);
continue;
}
db.insert(tachesRecurrentes).values({
nom: tache.nom,
categorie_id: catId,
duree_moyenne_min: tache.duree,
coefficient_penibilite: tache.coef,
}).run();
}
console.log(`✅ Seed terminé : ${CATEGORIES_DEFAUT.length} catégories, ${TACHES_DEFAUT.length} tâches`);
}
seed().catch(console.error);
+126
View File
@@ -0,0 +1,126 @@
// src/index.ts
// Serveur Express principal - API REST + service des fichiers statiques du frontend
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import path from 'path';
import fs from 'fs';
import { initDb } from './db/index';
import { db } from './db/index';
import { categories, tachesRecurrentes } from './db/schema';
import foyerRouter from './routes/foyer';
import membresRouter from './routes/membres';
import categoriesRouter from './routes/categories';
import tachesRouter from './routes/taches';
import saisiesRouter from './routes/saisies';
const app = express();
const PORT = process.env.PORT || 3001;
// ─── Middlewares ─────────────────────────────────────────────────────────────
app.use(helmet({
contentSecurityPolicy: false, // Désactivé pour simplifier le développement
}));
app.use(cors({
origin: process.env.NODE_ENV === 'production' ? false : '*',
}));
app.use(morgan('dev'));
app.use(express.json({ limit: '5mb' }));
// ─── Initialisation DB ───────────────────────────────────────────────────────
initDb();
// Seed automatique si aucune catégorie n'existe
const existantes = db.select().from(categories).limit(1).all();
if (existantes.length === 0) {
console.log('🌱 Première exécution - seed automatique...');
try {
// Seed synchrone inline (évite les problèmes d'import dynamique)
const CATS = [
{ nom: 'Cuisine', icone: '🍳', couleur: '#ef4444', ordre: 1 },
{ nom: 'Ménage', icone: '🧹', couleur: '#f97316', ordre: 2 },
{ nom: 'Courses', icone: '🛒', couleur: '#eab308', ordre: 3 },
{ nom: 'Enfants', icone: '👶', couleur: '#22c55e', ordre: 4 },
{ nom: 'Administratif', icone: '📋', couleur: '#3b82f6', ordre: 5 },
{ nom: 'Entretien maison', icone: '🔧', couleur: '#8b5cf6', ordre: 6 },
{ nom: 'Charge mentale', icone: '🧠', couleur: '#ec4899', ordre: 7 },
];
const TACHES = [
{ nom: 'Préparation repas', cat: 'Cuisine', duree: 45, coef: 3 },
{ nom: 'Vaisselle / Plonge', cat: 'Cuisine', duree: 20, coef: 2 },
{ nom: 'Rangement cuisine', cat: 'Cuisine', duree: 15, coef: 1 },
{ nom: 'Batch cooking semaine', cat: 'Cuisine', duree: 90, coef: 3 },
{ nom: 'Aspiration / Balayage sols', cat: 'Ménage', duree: 30, coef: 2 },
{ nom: 'Nettoyage salle de bain', cat: 'Ménage', duree: 25, coef: 3 },
{ nom: 'Nettoyage WC', cat: 'Ménage', duree: 10, coef: 4 },
{ nom: 'Lessive (tri + machine)', cat: 'Ménage', duree: 20, coef: 2 },
{ nom: 'Étendage / Rangement linge', cat: 'Ménage', duree: 25, coef: 2 },
{ nom: 'Repassage', cat: 'Ménage', duree: 45, coef: 3 },
{ nom: 'Sortie des poubelles', cat: 'Ménage', duree: 10, coef: 2 },
{ nom: 'Courses alimentaires', cat: 'Courses', duree: 60, coef: 2 },
{ nom: 'Gestion liste de courses', cat: 'Courses', duree: 15, coef: 2 },
{ nom: 'Commandes en ligne', cat: 'Courses', duree: 20, coef: 2 },
{ nom: 'Bain / Douche enfants', cat: 'Enfants', duree: 30, coef: 3 },
{ nom: 'Aide aux devoirs', cat: 'Enfants', duree: 45, coef: 3 },
{ nom: 'Conduite activités extrascolaires', cat: 'Enfants', duree: 60, coef: 2 },
{ nom: 'Préparation cartable / affaires', cat: 'Enfants', duree: 15, coef: 2 },
{ nom: 'Planifier RDV médicaux', cat: 'Administratif', duree: 20, coef: 3 },
{ nom: 'Gestion papiers / courrier', cat: 'Administratif', duree: 30, coef: 3 },
{ nom: 'Déclarations / impôts', cat: 'Administratif', duree: 60, coef: 4 },
{ nom: 'Suivi factures / finances', cat: 'Administratif', duree: 30, coef: 3 },
{ nom: 'Bricolage / Réparations', cat: 'Entretien maison', duree: 60, coef: 3 },
{ nom: 'Jardinage / Balcon', cat: 'Entretien maison', duree: 60, coef: 2 },
{ nom: 'Nettoyage vitres', cat: 'Entretien maison', duree: 45, coef: 3 },
{ nom: 'Planification semaine famille', cat: 'Charge mentale', duree: 30, coef: 4 },
{ nom: 'Suivi scolarité enfants', cat: 'Charge mentale', duree: 20, coef: 3 },
{ nom: 'Gestion santé famille', cat: 'Charge mentale', duree: 30, coef: 4 },
{ nom: 'Organisation vacances / sorties', cat: 'Charge mentale', duree: 45, coef: 3 },
];
for (const cat of CATS) db.insert(categories).values(cat).run();
const catsDb = db.select().from(categories).all();
const catMap = new Map(catsDb.map(c => [c.nom, c.id]));
for (const t of TACHES) {
const catId = catMap.get(t.cat);
if (catId) db.insert(tachesRecurrentes).values({ nom: t.nom, categorie_id: catId, duree_moyenne_min: t.duree, coefficient_penibilite: t.coef }).run();
}
console.log(`✅ Seed : ${CATS.length} catégories, ${TACHES.length} tâches`);
} catch (e) {
console.warn('Seed échoué:', e);
}
}
// ─── Routes API ──────────────────────────────────────────────────────────────
app.use('/api/foyer', foyerRouter);
app.use('/api/membres', membresRouter);
app.use('/api/categories', categoriesRouter);
app.use('/api/taches', tachesRouter);
app.use('/api/saisies', saisiesRouter);
// ─── Santé ────────────────────────────────────────────────────────────────────
app.get('/api/health', (_req, res) => {
res.json({ ok: true, version: '1.0.0', timestamp: new Date().toISOString() });
});
// ─── Frontend statique (en production) ──────────────────────────────────────
const frontendPath = path.join(__dirname, '..', 'public');
if (fs.existsSync(frontendPath)) {
app.use(express.static(frontendPath));
// SPA fallback - toutes les routes non-API servent index.html
app.get('*', (req, res) => {
if (!req.path.startsWith('/api')) {
res.sendFile(path.join(frontendPath, 'index.html'));
}
});
}
// ─── Démarrage ───────────────────────────────────────────────────────────────
app.listen(PORT, () => {
console.log(`🚀 EquiTask API démarrée sur le port ${PORT}`);
console.log(` Mode : ${process.env.NODE_ENV || 'development'}`);
});
export default app;
+72
View File
@@ -0,0 +1,72 @@
// src/routes/categories.ts
// CRUD pour les catégories de tâches
import { Router } from 'express';
import { eq, asc } from 'drizzle-orm';
import { db } from '../db/index';
import { categories } from '../db/schema';
const router = Router();
router.get('/', (_req, res) => {
try {
const result = db.select().from(categories)
.where(eq(categories.actif, 1))
.orderBy(asc(categories.ordre), asc(categories.id))
.all();
res.json({ categories: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
router.post('/', (req, res) => {
try {
const { nom, icone, couleur, ordre } = req.body as {
nom: string; icone: string; couleur: string; ordre?: number;
};
if (!nom?.trim()) return res.status(400).json({ erreur: 'Nom obligatoire' });
if (!icone) return res.status(400).json({ erreur: 'Icône obligatoire' });
if (!couleur) return res.status(400).json({ erreur: 'Couleur obligatoire' });
const result = db.insert(categories).values({
nom: nom.trim(), icone, couleur, ordre: ordre ?? 0,
}).returning().get();
res.status(201).json({ categorie: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
router.put('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
const { nom, icone, couleur, ordre, actif } = req.body as {
nom?: string; icone?: string; couleur?: string; ordre?: number; actif?: number;
};
const updates: Record<string, unknown> = {};
if (nom !== undefined) updates.nom = nom.trim();
if (icone !== undefined) updates.icone = icone;
if (couleur !== undefined) updates.couleur = couleur;
if (ordre !== undefined) updates.ordre = ordre;
if (actif !== undefined) updates.actif = actif;
const result = db.update(categories).set(updates).where(eq(categories.id, id)).returning().get();
if (!result) return res.status(404).json({ erreur: 'Catégorie introuvable' });
res.json({ categorie: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
router.delete('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
db.update(categories).set({ actif: 0 }).where(eq(categories.id, id)).run();
res.json({ ok: true });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
export default router;
+51
View File
@@ -0,0 +1,51 @@
// src/routes/foyer.ts
import { Router } from 'express';
import { eq } from 'drizzle-orm';
import { db } from '../db/index';
import { foyer } from '../db/schema';
const router = Router();
/** GET /api/foyer - Récupère le foyer (null si non configuré) */
router.get('/', (_req, res) => {
try {
const result = db.select().from(foyer).limit(1).all();
res.json({ foyer: result[0] || null });
} catch (err) {
res.status(500).json({ erreur: 'Erreur serveur', detail: String(err) });
}
});
/** POST /api/foyer - Crée le foyer (première configuration) */
router.post('/', (req, res) => {
try {
const { nom } = req.body as { nom: string };
if (!nom?.trim()) {
return res.status(400).json({ erreur: 'Le nom du foyer est obligatoire' });
}
const existant = db.select().from(foyer).limit(1).all();
if (existant.length > 0) {
return res.status(400).json({ erreur: 'Le foyer est déjà configuré', foyer: existant[0] });
}
const result = db.insert(foyer).values({ nom: nom.trim() }).returning().get();
res.status(201).json({ foyer: result });
} catch (err) {
res.status(500).json({ erreur: 'Erreur serveur', detail: String(err) });
}
});
/** PUT /api/foyer/:id - Modifie le nom du foyer */
router.put('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
const { nom } = req.body as { nom: string };
if (!nom?.trim()) return res.status(400).json({ erreur: 'Nom obligatoire' });
const result = db.update(foyer).set({ nom: nom.trim() }).where(eq(foyer.id, id)).returning().get();
if (!result) return res.status(404).json({ erreur: 'Foyer introuvable' });
res.json({ foyer: result });
} catch (err) {
res.status(500).json({ erreur: 'Erreur serveur', detail: String(err) });
}
});
export default router;
+76
View File
@@ -0,0 +1,76 @@
// src/routes/membres.ts
// CRUD pour les membres du foyer
import { Router } from 'express';
import { eq, asc } from 'drizzle-orm';
import { db } from '../db/index';
import { membres } from '../db/schema';
const router = Router();
/** GET /api/membres - Liste tous les membres actifs */
router.get('/', (_req, res) => {
try {
const result = db.select().from(membres)
.where(eq(membres.actif, 1))
.orderBy(asc(membres.ordre), asc(membres.id))
.all();
res.json({ membres: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
/** POST /api/membres - Crée un nouveau membre */
router.post('/', (req, res) => {
try {
const { nom, role, couleur, ordre } = req.body as {
nom: string; role: string; couleur: string; ordre?: number;
};
if (!nom?.trim()) return res.status(400).json({ erreur: 'Nom obligatoire' });
if (!['adulte', 'enfant'].includes(role)) return res.status(400).json({ erreur: 'Rôle invalide' });
if (!couleur) return res.status(400).json({ erreur: 'Couleur obligatoire' });
const result = db.insert(membres).values({
nom: nom.trim(), role, couleur, ordre: ordre ?? 0, actif: 1,
}).returning().get();
res.status(201).json({ membre: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
/** PUT /api/membres/:id - Modifie un membre */
router.put('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
const { nom, role, couleur, actif, ordre } = req.body as {
nom?: string; role?: string; couleur?: string; actif?: number; ordre?: number;
};
const updates: Record<string, unknown> = {};
if (nom !== undefined) updates.nom = nom.trim();
if (role !== undefined) updates.role = role;
if (couleur !== undefined) updates.couleur = couleur;
if (actif !== undefined) updates.actif = actif;
if (ordre !== undefined) updates.ordre = ordre;
const result = db.update(membres).set(updates).where(eq(membres.id, id)).returning().get();
if (!result) return res.status(404).json({ erreur: 'Membre introuvable' });
res.json({ membre: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
/** DELETE /api/membres/:id - Désactive un membre (soft delete) */
router.delete('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
db.update(membres).set({ actif: 0 }).where(eq(membres.id, id)).run();
res.json({ ok: true });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
export default router;
+380
View File
@@ -0,0 +1,380 @@
// src/routes/saisies.ts
// Routes pour les saisies de tâches + dashboard stats + export
import { Router, Request, Response } from 'express';
import { eq, and, gte, lte, desc, asc, sql, inArray } from 'drizzle-orm';
import { db } from '../db/index';
import { saisies, membres, categories, tachesRecurrentes, foyer } from '../db/schema';
const router = Router();
// ─── SAISIES CRUD ─────────────────────────────────────────────────────────────
/** GET /api/saisies - Liste des saisies avec filtres optionnels */
router.get('/', (req, res) => {
try {
const { debut, fin, membre_id, categorie_id, type, page = '1', limit = '50' } = req.query as Record<string, string>;
const pageNum = parseInt(page);
const limitNum = Math.min(parseInt(limit), 200);
const offset = (pageNum - 1) * limitNum;
let query = db.select({
id: saisies.id,
tache_recurrente_id: saisies.tache_recurrente_id,
nom_tache_oneshot: saisies.nom_tache_oneshot,
categorie_id: saisies.categorie_id,
membre_id: saisies.membre_id,
date_heure: saisies.date_heure,
duree_reelle_min: saisies.duree_reelle_min,
coefficient_penibilite: saisies.coefficient_penibilite,
score_final: saisies.score_final,
notes: saisies.notes,
synced: saisies.synced,
cree_le: saisies.cree_le,
// Jointures
membre_nom: membres.nom,
membre_couleur: membres.couleur,
membre_role: membres.role,
categorie_nom: categories.nom,
categorie_icone: categories.icone,
categorie_couleur: categories.couleur,
tache_nom: tachesRecurrentes.nom,
})
.from(saisies)
.leftJoin(membres, eq(saisies.membre_id, membres.id))
.leftJoin(categories, eq(saisies.categorie_id, categories.id))
.leftJoin(tachesRecurrentes, eq(saisies.tache_recurrente_id, tachesRecurrentes.id));
// Filtres
const conditions = [];
if (debut) conditions.push(gte(saisies.date_heure, debut));
if (fin) conditions.push(lte(saisies.date_heure, fin + 'T23:59:59'));
if (membre_id) conditions.push(eq(saisies.membre_id, parseInt(membre_id)));
if (categorie_id) conditions.push(eq(saisies.categorie_id, parseInt(categorie_id)));
if (type === 'recurrente') conditions.push(sql`${saisies.tache_recurrente_id} IS NOT NULL`);
if (type === 'oneshot') conditions.push(sql`${saisies.nom_tache_oneshot} IS NOT NULL`);
const result = conditions.length > 0
? query.where(and(...conditions)).orderBy(desc(saisies.date_heure)).limit(limitNum).offset(offset).all()
: query.orderBy(desc(saisies.date_heure)).limit(limitNum).offset(offset).all();
// Compter le total
const total = db.select({ count: sql<number>`count(*)` }).from(saisies)
.where(conditions.length > 0 ? and(...conditions) : undefined).get()?.count ?? 0;
res.json({ saisies: result, total, page: pageNum, limit: limitNum });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
/** POST /api/saisies - Crée une saisie */
router.post('/', (req, res) => {
try {
const body = req.body as {
tache_recurrente_id?: number;
nom_tache_oneshot?: string;
categorie_id: number;
membre_id: number;
date_heure?: string;
duree_reelle_min?: number;
coefficient_penibilite: number;
score_final: number;
notes?: string;
};
if (!body.categorie_id) return res.status(400).json({ erreur: 'Catégorie obligatoire' });
if (!body.membre_id) return res.status(400).json({ erreur: 'Membre obligatoire' });
if (!body.tache_recurrente_id && !body.nom_tache_oneshot) {
return res.status(400).json({ erreur: 'Tâche récurrente ou nom ponctuel obligatoire' });
}
if (body.coefficient_penibilite < 1 || body.coefficient_penibilite > 5) {
return res.status(400).json({ erreur: 'Coefficient invalide (1-5)' });
}
const result = db.insert(saisies).values({
tache_recurrente_id: body.tache_recurrente_id || null,
nom_tache_oneshot: body.nom_tache_oneshot || null,
categorie_id: body.categorie_id,
membre_id: body.membre_id,
date_heure: body.date_heure || new Date().toISOString().slice(0, 19).replace('T', ' '),
duree_reelle_min: body.duree_reelle_min || null,
coefficient_penibilite: body.coefficient_penibilite,
score_final: body.score_final,
notes: body.notes || null,
synced: 1,
}).returning().get();
res.status(201).json({ saisie: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
/** POST /api/saisies/batch - Sync offline (multiple saisies) */
router.post('/batch', (req, res) => {
try {
const { saisiesData } = req.body as { saisiesData: any[] };
if (!Array.isArray(saisiesData)) return res.status(400).json({ erreur: 'Array attendu' });
let count = 0;
for (const s of saisiesData) {
try {
db.insert(saisies).values({ ...s, synced: 1 }).run();
count++;
} catch (e) {
console.warn('Saisie batch ignorée:', e);
}
}
res.json({ ok: true, count });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
/** PUT /api/saisies/:id - Modifie une saisie */
router.put('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
const { duree_reelle_min, coefficient_penibilite, score_final, notes, date_heure } = req.body;
const updates: Record<string, unknown> = {};
if (duree_reelle_min !== undefined) updates.duree_reelle_min = duree_reelle_min;
if (coefficient_penibilite !== undefined) updates.coefficient_penibilite = coefficient_penibilite;
if (score_final !== undefined) updates.score_final = score_final;
if (notes !== undefined) updates.notes = notes;
if (date_heure !== undefined) updates.date_heure = date_heure;
const result = db.update(saisies).set(updates).where(eq(saisies.id, id)).returning().get();
if (!result) return res.status(404).json({ erreur: 'Saisie introuvable' });
res.json({ saisie: result });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
/** DELETE /api/saisies/:id */
router.delete('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
db.delete(saisies).where(eq(saisies.id, id)).run();
res.json({ ok: true });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
// ─── DASHBOARD STATS ──────────────────────────────────────────────────────────
/** GET /api/saisies/stats - Statistiques pour le dashboard */
router.get('/stats', (req, res) => {
try {
const { debut, fin, inclure_enfants = 'false', categorie_id } = req.query as Record<string, string>;
// Construire conditions de base
const conditions: any[] = [];
if (debut) conditions.push(gte(saisies.date_heure, debut));
if (fin) conditions.push(lte(saisies.date_heure, fin + 'T23:59:59'));
if (categorie_id) conditions.push(eq(saisies.categorie_id, parseInt(categorie_id)));
if (inclure_enfants !== 'true') conditions.push(eq(membres.role, 'adulte'));
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
// 1. Scores par membre
const scoresMembres = db.select({
membre_id: membres.id,
nom: membres.nom,
couleur: membres.couleur,
role: membres.role,
score_total: sql<number>`COALESCE(SUM(${saisies.score_final}), 0)`,
nb_saisies: sql<number>`COUNT(${saisies.id})`,
})
.from(membres)
.leftJoin(saisies, and(
eq(saisies.membre_id, membres.id),
debut ? gte(saisies.date_heure, debut) : undefined,
fin ? lte(saisies.date_heure, fin + 'T23:59:59') : undefined,
categorie_id ? eq(saisies.categorie_id, parseInt(categorie_id)) : undefined,
))
.where(and(
eq(membres.actif, 1),
inclure_enfants !== 'true' ? eq(membres.role, 'adulte') : undefined,
))
.groupBy(membres.id)
.all();
const scoreTotal = scoresMembres.reduce((acc, m) => acc + (m.score_total || 0), 0);
const avecPct = scoresMembres.map(m => ({
...m,
pourcentage: scoreTotal > 0 ? Math.round((m.score_total / scoreTotal) * 100) : 0,
}));
// 2. Scores par catégorie × membre
const scoresParCat = db.select({
categorie_id: categories.id,
categorie_nom: categories.nom,
categorie_icone: categories.icone,
categorie_couleur: categories.couleur,
membre_id: membres.id,
membre_nom: membres.nom,
membre_couleur: membres.couleur,
score: sql<number>`COALESCE(SUM(${saisies.score_final}), 0)`,
})
.from(categories)
.leftJoin(saisies, and(
eq(saisies.categorie_id, categories.id),
debut ? gte(saisies.date_heure, debut) : undefined,
fin ? lte(saisies.date_heure, fin + 'T23:59:59') : undefined,
))
.leftJoin(membres, and(
eq(saisies.membre_id, membres.id),
eq(membres.actif, 1),
inclure_enfants !== 'true' ? eq(membres.role, 'adulte') : undefined,
))
.where(eq(categories.actif, 1))
.groupBy(categories.id, membres.id)
.all();
// Restructurer par catégorie
const catMap = new Map<number, any>();
for (const row of scoresParCat) {
if (!catMap.has(row.categorie_id)) {
catMap.set(row.categorie_id, {
categorie_id: row.categorie_id,
nom: row.categorie_nom,
icone: row.categorie_icone,
couleur: row.categorie_couleur,
scores_membres: [],
});
}
if (row.membre_id) {
catMap.get(row.categorie_id).scores_membres.push({
membre_id: row.membre_id,
nom: row.membre_nom,
couleur: row.membre_couleur,
score: row.score,
});
}
}
const scoresCategories = Array.from(catMap.values()).filter(c =>
c.scores_membres.some((m: any) => m.score > 0)
);
// 3. Évolution temporelle (7 derniers jours ou période sélectionnée)
const membresActifs = db.select().from(membres)
.where(and(
eq(membres.actif, 1),
inclure_enfants !== 'true' ? eq(membres.role, 'adulte') : undefined,
)).all();
const evolutionRaw = db.select({
date: sql<string>`date(${saisies.date_heure})`,
membre_id: saisies.membre_id,
score_jour: sql<number>`SUM(${saisies.score_final})`,
})
.from(saisies)
.leftJoin(membres, eq(saisies.membre_id, membres.id))
.where(and(
debut ? gte(saisies.date_heure, debut) : undefined,
fin ? lte(saisies.date_heure, fin + 'T23:59:59') : undefined,
eq(membres.actif, 1),
inclure_enfants !== 'true' ? eq(membres.role, 'adulte') : undefined,
categorie_id ? eq(saisies.categorie_id, parseInt(categorie_id)) : undefined,
))
.groupBy(sql`date(${saisies.date_heure})`, saisies.membre_id)
.orderBy(asc(sql`date(${saisies.date_heure})`))
.all();
// Grouper par date
const dateMap = new Map<string, Map<number, number>>();
for (const row of evolutionRaw) {
if (!dateMap.has(row.date)) dateMap.set(row.date, new Map());
dateMap.get(row.date)!.set(row.membre_id, row.score_jour);
}
const evolution = Array.from(dateMap.entries()).map(([date, scoresByMembre]) => ({
date,
scores: membresActifs.map(m => ({
membre_id: m.id,
nom: m.nom,
couleur: m.couleur,
score: scoresByMembre.get(m.id) || 0,
})),
}));
// 4. Indicateur d'équilibre (adultes seulement)
const adultes = avecPct.filter(m => m.role === 'adulte');
let indicateur = null;
if (adultes.length >= 2) {
const [a1, a2] = adultes.sort((a, b) => b.score_total - a.score_total);
const totalAdultes = a1.score_total + a2.score_total;
const ecart = totalAdultes > 0
? Math.abs(Math.round(((a1.score_total - a2.score_total) / totalAdultes) * 100))
: 0;
indicateur = {
adulte1: { membre_id: a1.membre_id, nom: a1.nom, couleur: a1.couleur, score: a1.score_total, pourcentage: a1.pourcentage },
adulte2: { membre_id: a2.membre_id, nom: a2.nom, couleur: a2.couleur, score: a2.score_total, pourcentage: a2.pourcentage },
ecart_pct: ecart,
statut: ecart <= 10 ? 'vert' : ecart <= 25 ? 'orange' : 'rouge',
};
}
res.json({
scores_par_membre: avecPct,
scores_par_categorie: scoresCategories,
evolution_temporelle: evolution,
indicateur_equilibre: indicateur,
});
} catch (err) {
console.error(err);
res.status(500).json({ erreur: String(err) });
}
});
// ─── EXPORT ───────────────────────────────────────────────────────────────────
/** GET /api/saisies/export?format=json|csv */
router.get('/export', (req, res) => {
try {
const format = req.query.format === 'csv' ? 'csv' : 'json';
const data = db.select({
id: saisies.id,
date: saisies.date_heure,
membre: membres.nom,
role: membres.role,
tache: sql<string>`COALESCE(${tachesRecurrentes.nom}, ${saisies.nom_tache_oneshot})`,
categorie: categories.nom,
type: sql<string>`CASE WHEN ${saisies.tache_recurrente_id} IS NOT NULL THEN 'recurrente' ELSE 'oneshot' END`,
duree: sql<number>`COALESCE(${saisies.duree_reelle_min}, 0)`,
penibilite: saisies.coefficient_penibilite,
score: saisies.score_final,
notes: saisies.notes,
})
.from(saisies)
.leftJoin(membres, eq(saisies.membre_id, membres.id))
.leftJoin(categories, eq(saisies.categorie_id, categories.id))
.leftJoin(tachesRecurrentes, eq(saisies.tache_recurrente_id, tachesRecurrentes.id))
.orderBy(desc(saisies.date_heure))
.all();
if (format === 'json') {
res.setHeader('Content-Disposition', 'attachment; filename="equitask-export.json"');
res.setHeader('Content-Type', 'application/json');
res.json(data);
} else {
// CSV
const headers = ['id', 'date', 'membre', 'role', 'tache', 'categorie', 'type', 'duree_min', 'penibilite', 'score', 'notes'];
const rows = data.map(row => [
row.id, row.date, row.membre, row.role, row.tache, row.categorie,
row.type, row.duree, row.penibilite, row.score, row.notes || '',
].map(v => `"${String(v ?? '').replace(/"/g, '""')}"`).join(','));
res.setHeader('Content-Disposition', 'attachment; filename="equitask-export.csv"');
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.send('' + [headers.join(','), ...rows].join('\n')); // BOM pour Excel
}
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
export default router;
+101
View File
@@ -0,0 +1,101 @@
// src/routes/taches.ts
// CRUD pour les tâches récurrentes
import { Router } from 'express';
import { eq, asc } from 'drizzle-orm';
import { db } from '../db/index';
import { tachesRecurrentes, categories } from '../db/schema';
const router = Router();
/** Calcule le score d'une tâche */
const calcScore = (duree: number, coef: number) => duree * coef;
router.get('/', (_req, res) => {
try {
const taches = db.select({
id: tachesRecurrentes.id,
nom: tachesRecurrentes.nom,
categorie_id: tachesRecurrentes.categorie_id,
duree_moyenne_min: tachesRecurrentes.duree_moyenne_min,
coefficient_penibilite: tachesRecurrentes.coefficient_penibilite,
actif: tachesRecurrentes.actif,
// Jointure avec categories
categorie_nom: categories.nom,
categorie_icone: categories.icone,
categorie_couleur: categories.couleur,
})
.from(tachesRecurrentes)
.leftJoin(categories, eq(tachesRecurrentes.categorie_id, categories.id))
.where(eq(tachesRecurrentes.actif, 1))
.orderBy(asc(categories.ordre), asc(tachesRecurrentes.nom))
.all();
// Ajouter score_calcule
const avecScore = taches.map(t => ({
...t,
score_calcule: calcScore(t.duree_moyenne_min, t.coefficient_penibilite),
}));
res.json({ taches: avecScore });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
router.post('/', (req, res) => {
try {
const { nom, categorie_id, duree_moyenne_min, coefficient_penibilite } = req.body as {
nom: string; categorie_id: number; duree_moyenne_min: number; coefficient_penibilite: number;
};
if (!nom?.trim()) return res.status(400).json({ erreur: 'Nom obligatoire' });
if (!categorie_id) return res.status(400).json({ erreur: 'Catégorie obligatoire' });
if (!duree_moyenne_min || duree_moyenne_min < 1) return res.status(400).json({ erreur: 'Durée invalide' });
if (!coefficient_penibilite || coefficient_penibilite < 1 || coefficient_penibilite > 5) {
return res.status(400).json({ erreur: 'Coefficient 1-5 obligatoire' });
}
const result = db.insert(tachesRecurrentes).values({
nom: nom.trim(), categorie_id, duree_moyenne_min, coefficient_penibilite,
}).returning().get();
res.status(201).json({
tache: { ...result, score_calcule: calcScore(result.duree_moyenne_min, result.coefficient_penibilite) }
});
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
router.put('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
const { nom, categorie_id, duree_moyenne_min, coefficient_penibilite, actif } = req.body as {
nom?: string; categorie_id?: number; duree_moyenne_min?: number; coefficient_penibilite?: number; actif?: number;
};
const updates: Record<string, unknown> = {};
if (nom !== undefined) updates.nom = nom.trim();
if (categorie_id !== undefined) updates.categorie_id = categorie_id;
if (duree_moyenne_min !== undefined) updates.duree_moyenne_min = duree_moyenne_min;
if (coefficient_penibilite !== undefined) updates.coefficient_penibilite = coefficient_penibilite;
if (actif !== undefined) updates.actif = actif;
const result = db.update(tachesRecurrentes).set(updates).where(eq(tachesRecurrentes.id, id)).returning().get();
if (!result) return res.status(404).json({ erreur: 'Tâche introuvable' });
res.json({ tache: { ...result, score_calcule: calcScore(result.duree_moyenne_min, result.coefficient_penibilite) } });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
router.delete('/:id', (req, res) => {
try {
const id = parseInt(req.params.id);
db.update(tachesRecurrentes).set({ actif: 0 }).where(eq(tachesRecurrentes.id, id)).run();
res.json({ ok: true });
} catch (err) {
res.status(500).json({ erreur: String(err) });
}
});
export default router;
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}