Initial commit: Club DynaSQY

This commit is contained in:
2026-08-23 09:04:47 +00:00
commit 0875890dc7
145 changed files with 16947 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
const pool = require('../config/db');
const { logActivity } = require('../services/activityLog');
exports.listByMember = async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT a.*,
u.prenom AS created_prenom,
r.prenom AS remplace_prenom, r.nom AS remplace_nom,
rg.prenom AS remplace_guest_prenom, rg.nom AS remplace_guest_nom
FROM absences a
LEFT JOIN users u ON u.id = a.created_by
LEFT JOIN users r ON r.id = a.remplace_par_id AND (a.remplace_par_type = 'member' OR a.remplace_par_type IS NULL)
LEFT JOIN invites rg ON rg.id = a.remplace_par_guest_id AND a.remplace_par_type = 'guest'
WHERE a.membre_id = ?
ORDER BY a.date_absence DESC
`, [req.params.id]);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.create = async (req, res) => {
try {
const { membre_id, date_absence, motif, prevenu, remplace_par_type, remplace_par_guest_id, remplace_par_id } = req.body;
const rType = remplace_par_type === 'guest' && remplace_par_guest_id ? 'guest' : 'member'
const rMemberId = rType === 'member' ? (remplace_par_id || null) : null
const rGuestId = rType === 'guest' ? (remplace_par_guest_id || null) : null
await pool.query(
'INSERT INTO absences (membre_id, date_absence, motif, prevenu, remplace_par_id, remplace_par_type, remplace_par_guest_id, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[membre_id, date_absence, motif || null, prevenu ? 1 : 0, rMemberId, rType, rGuestId, req.user.id]
);
logActivity(req, 'absence_ajout', { membre_id, date_absence });
res.status(201).json({ message: 'Absence enregistrée' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.remove = async (req, res) => {
try {
await pool.query('DELETE FROM absences WHERE id = ?', [req.params.id]);
logActivity(req, 'absence_suppression', { absenceId: req.params.id });
res.json({ message: 'Absence supprimée' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.stats = async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT u.id, u.prenom, u.nom, COUNT(a.id) AS total_absences
FROM users u
LEFT JOIN absences a ON a.membre_id = u.id
WHERE u.actif = 1
GROUP BY u.id
ORDER BY total_absences DESC
LIMIT 10
`);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
@@ -0,0 +1,32 @@
const pool = require('../config/db');
exports.getLogs = async (req, res) => {
try {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, Math.max(10, parseInt(req.query.limit) || 50));
const offset = (page - 1) * limit;
const actionFilter = req.query.action || '';
let where = '';
const params = [];
if (actionFilter) {
where = 'WHERE al.action LIKE ?';
params.push(`%${actionFilter}%`);
}
const [countRows] = await pool.query(
`SELECT COUNT(*) AS total FROM activity_logs al ${where}`, params
);
const total = countRows[0].total;
const [rows] = await pool.query(
`SELECT al.* FROM activity_logs al ${where} ORDER BY al.created_at DESC LIMIT ? OFFSET ?`,
[...params, limit, offset]
);
res.json({ logs: rows, total, page, limit, pages: Math.ceil(total / limit) });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur' });
}
};
+32
View File
@@ -0,0 +1,32 @@
const pool = require('../config/db');
const { logActivity } = require('../services/activityLog');
exports.listUsers = async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT id, prenom, nom, societe, email, telephone, role, actif, date_adhesion, date_echeance FROM users ORDER BY prenom ASC'
);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.updateUserRole = async (req, res) => {
try {
const { role } = req.body;
if (!['admin', 'member'].includes(role)) {
return res.status(400).json({ error: 'Role must be admin or member' });
}
const userId = req.params.id;
if (Number(userId) === req.user.id) {
return res.status(400).json({ error: 'Vous ne pouvez pas modifier votre propre rôle' });
}
await pool.query('UPDATE users SET role = ? WHERE id = ?', [role, userId]);
logActivity(req, 'role_changement', { targetUserId: userId, newRole: role });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
+73
View File
@@ -0,0 +1,73 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const pool = require('../config/db');
const { logActivity } = require('../services/activityLog');
exports.login = async (req, res) => {
try {
const { identifier, password } = req.body;
if (!identifier || !password) {
return res.status(400).json({ error: 'Identifiant et mot de passe requis' });
}
const [rows] = await pool.query(
'SELECT * FROM users WHERE (prenom = ? OR email = ?) AND actif = 1',
[identifier, identifier]
);
if (rows.length === 0) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
const user = rows[0];
const match = await bcrypt.compare(password, user.password_hash);
if (!match) {
return res.status(401).json({ error: 'Identifiants invalides' });
}
const token = jwt.sign(
{ id: user.id, prenom: user.prenom, nom: user.nom, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
logActivity(user, 'connexion');
res.json({
token,
user: { id: user.id, prenom: user.prenom, nom: user.nom, societe: user.societe, email: user.email, role: user.role },
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.me = async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT id, prenom, nom, societe, activite, email, telephone, role, date_adhesion, date_echeance FROM users WHERE id = ?',
[req.user.id]
);
if (rows.length === 0) return res.status(404).json({ error: 'Utilisateur introuvable' });
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: 'Mot de passe actuel et nouveau requis' });
}
if (newPassword.length < 6) {
return res.status(400).json({ error: 'Le nouveau mot de passe doit faire au moins 6 caractères' });
}
const [rows] = await pool.query('SELECT password_hash FROM users WHERE id = ?', [req.user.id]);
const match = await bcrypt.compare(currentPassword, rows[0].password_hash);
if (!match) return res.status(401).json({ error: 'Mot de passe actuel incorrect' });
const hash = await bcrypt.hash(newPassword, 10);
await pool.query('UPDATE users SET password_hash = ? WHERE id = ?', [hash, req.user.id]);
logActivity(req, 'changement_mot_de_passe');
res.json({ message: 'Mot de passe modifié' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
+133
View File
@@ -0,0 +1,133 @@
const pool = require('../config/db');
const cheerio = require('cheerio');
const { logActivity } = require('../services/activityLog');
const MONTHS_FR = {
'janv': '01', 'janvier': '01', 'févr': '02', 'février': '02', 'fevrier': '02',
'mars': '03', 'avr': '04', 'avril': '04', 'mai': '05', 'juin': '06',
'juil': '07', 'juillet': '07', 'août': '08', 'aout': '08',
'sept': '09', 'septembre': '09', 'oct': '10', 'octobre': '10',
'nov': '11', 'novembre': '11', 'déc': '12', 'décembre': '12', 'decembre': '12'
};
function parseFrenchDate(str) {
const m = str.trim().match(/^(\d+)\s+(\S+)\s+(\d{4})$/);
if (!m) return null;
const month = MONTHS_FR[m[2].toLowerCase().replace(/\.$/, '')];
if (!month) return null;
return `${m[3]}-${month}-${m[1].padStart(2, '0')}`;
}
exports.list = async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM evenements ORDER BY date ASC');
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.create = async (req, res) => {
try {
const { date, nom_evenement, lieu, horaire } = req.body;
if (!date || !nom_evenement) {
return res.status(400).json({ error: 'Date and name required' });
}
const [result] = await pool.query(
'INSERT INTO evenements (date, nom_evenement, lieu, horaire) VALUES (?, ?, ?, ?)',
[date, nom_evenement, lieu || null, horaire || null]
);
res.status(201).json({ id: result.insertId, message: 'Event created' });
logActivity(req, 'evenement_ajout', { date, nom_evenement });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.update = async (req, res) => {
try {
const { date, nom_evenement, lieu, horaire } = req.body;
await pool.query(
'UPDATE evenements SET date = ?, nom_evenement = ?, lieu = ?, horaire = ? WHERE id = ?',
[date, nom_evenement, lieu || null, horaire || null, req.params.id]
);
res.json({ message: 'Updated' });
logActivity(req, 'evenement_modification', { id: req.params.id, date, nom_evenement });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.remove = async (req, res) => {
try {
await pool.query('DELETE FROM evenements WHERE id = ?', [req.params.id]);
res.json({ message: 'Deleted' });
logActivity(req, 'evenement_suppression', { id: req.params.id });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.deletePast = async (req, res) => {
try {
const [result] = await pool.query('DELETE FROM evenements WHERE date < CURDATE()');
res.json({ message: `${result.affectedRows} événement(s) supprimé(s)` });
logActivity(req, 'evenements_passes_supprimes', { count: result.affectedRows });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.scrape = async (req, res) => {
try {
const url = 'https://www.rencontres-dirigeants.com/nos-rencontres?region=11&startAt=&endAt=&dateRange=&format=&agency%5B%5D=289&department=';
const resp = await fetch(url);
const html = await resp.text();
const $ = cheerio.load(html);
const candidates = [];
$('article, [class*="card"], [class*="event"], [class*="rencontre"]').each((i, el) => {
const text = $(el).text();
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
let location = '', dateStr = '', timeStr = '';
for (const line of lines) {
if (!location && line.length > 3 && line[0] === line[0].toUpperCase() && !line.includes('€') && !line.includes('Participation') && !line.includes('places')) {
location = line;
}
const dateMatch = line.match(/(\d{1,2}\s+\S+\s+\d{4})/);
if (dateMatch && !dateStr) dateStr = dateMatch[1];
const timeMatch = line.match(/(\d{2}h\d{2})\s*[àa]\s*(\d{2}h\d{2})/);
if (timeMatch && !timeStr) timeStr = `${timeMatch[1]} - ${timeMatch[2]}`;
}
if (!dateStr) return;
const isoDate = parseFrenchDate(dateStr);
if (!isoDate) return;
candidates.push({ isoDate, location, timeStr });
});
const imported = [];
for (const c of candidates) {
const [existing] = await pool.query('SELECT id FROM evenements WHERE date = ? AND nom_evenement LIKE ?', [c.isoDate, `%Rencontre%`]);
if (existing.length > 0) continue;
const title = c.location ? `Rencontre dirigeants - ${c.location}` : 'Rencontre dirigeants Dynabuy';
await pool.query(
'INSERT INTO evenements (date, nom_evenement, lieu, horaire) VALUES (?, ?, ?, ?)',
[c.isoDate, title, c.location || null, c.timeStr || null]
);
imported.push({ date: c.isoDate, title, location: c.location, time: c.timeStr });
}
res.json({ message: `${imported.length} événement(s) importé(s)`, events: imported });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur lors de l\'import: ' + err.message });
}
};
+65
View File
@@ -0,0 +1,65 @@
const pool = require('../config/db');
function escapeCsv(val) {
if (val == null) return '';
const s = String(val);
return s.includes(',') || s.includes('"') || s.includes('\n') || s.includes('\r')
? `"${s.replace(/"/g, '""')}"`
: s;
}
function rowsToCsv(headers, rows) {
const lines = [headers.map(escapeCsv).join(',')];
for (const row of rows) {
lines.push(headers.map((h) => escapeCsv(row[h])).join(','));
}
return '\uFEFF' + lines.join('\r\n');
}
exports.exportCsv = async (req, res) => {
try {
const [members] = await pool.query(
'SELECT id, prenom, nom, email, telephone, societe, fonction, activite, site_web, role, actif, date_echeance, created_at FROM users ORDER BY nom, prenom'
);
const [guests] = await pool.query(
`SELECT i.id, i.prenom, i.nom, i.email, i.telephone, i.societe, i.activite, i.site_web, i.statut, i.visit_number, i.invite_par_id, u.prenom AS invite_par_prenom, u.nom AS invite_par_nom, i.date_club, i.commentaires, i.created_at
FROM invites i
LEFT JOIN users u ON u.id = i.invite_par_id
ORDER BY i.date_club DESC, i.nom, i.prenom`
);
const [recs] = await pool.query(
`SELECT r.id, r.date, r.nom_prospect, r.type, r.transformee, r.ca_realise,
m.prenom AS membre_prenom, m.nom AS membre_nom, m.societe AS membre_societe,
d.prenom AS dest_prenom, d.nom AS dest_nom, d.societe AS dest_societe
FROM recommandations r
LEFT JOIN users m ON m.id = r.membre_id
LEFT JOIN users d ON d.id = r.destinataire_id
ORDER BY r.date DESC`
);
const membresCsv = rowsToCsv(
['id', 'prenom', 'nom', 'email', 'telephone', 'societe', 'fonction', 'activite', 'site_web', 'role', 'actif', 'date_echeance', 'date_inscription'],
members
);
const invitesCsv = rowsToCsv(
['id', 'prenom', 'nom', 'email', 'telephone', 'societe', 'activite', 'site_web', 'statut', 'visite_numero', 'invite_par_id', 'invite_par_prenom', 'invite_par_nom', 'date_club', 'commentaires', 'date_creation'],
guests
);
const recosCsv = rowsToCsv(
['id', 'date', 'nom_prospect', 'type', 'transformee', 'ca_realise', 'membre_prenom', 'membre_nom', 'membre_societe', 'destinataire_prenom', 'destinataire_nom', 'destinataire_societe'],
recs
);
const date = new Date().toISOString().slice(0, 10);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="dyna-biz-export-${date}.csv"`);
res.write('=== MEMBRES ===\r\n' + membresCsv + '\r\n\r\n');
res.write('=== INVITES ===\r\n' + invitesCsv + '\r\n\r\n');
res.write('=== RECOMMANDATIONS ===\r\n' + recosCsv);
res.end();
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur export' });
}
};
+77
View File
@@ -0,0 +1,77 @@
const fs = require('fs');
const path = require('path');
const pool = require('../config/db');
const galleryDir = path.join(__dirname, '..', 'gallery');
exports.list = async (req, res) => {
try {
if (!fs.existsSync(galleryDir)) return res.json([]);
const dates = fs.readdirSync(galleryDir).filter((d) => fs.statSync(path.join(galleryDir, d)).isDirectory()).sort().reverse();
const result = [];
for (const date of dates) {
const dirPath = path.join(galleryDir, date);
const files = fs.readdirSync(dirPath)
.filter((f) => /\.(jpg|jpeg|png|gif|webp|heic)$/i.test(f) && f !== '.thumbnails')
.map((f) => ({ filename: f, url: `/gallery/${date}/${encodeURIComponent(f)}` }))
.sort();
if (files.length > 0) result.push({ date, files });
}
res.json(result);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.upload = async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'Aucun fichier sélectionné' });
}
const { date_club } = req.body;
if (!date_club) return res.status(400).json({ error: 'Date du rendez-vous requise' });
const dateFolder = date_club.replace(/-/g, '');
const targetDir = path.join(galleryDir, dateFolder);
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
const saved = [];
for (const file of req.files) {
const ext = path.extname(file.originalname);
const baseName = path.basename(file.originalname, ext);
let destPath = path.join(targetDir, file.originalname);
let counter = 1;
while (fs.existsSync(destPath)) {
destPath = path.join(targetDir, `${baseName}_${counter}${ext}`);
counter++;
}
fs.renameSync(file.path, destPath);
saved.push({ filename: path.basename(destPath), url: `/gallery/${dateFolder}/${path.basename(destPath)}` });
}
res.json({ message: `${saved.length} photo(s) ajoutée(s)`, files: saved });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur upload' });
}
};
exports.deletePhoto = async (req, res) => {
try {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
const { date, filename } = req.params;
const filePath = path.join(galleryDir, date, filename);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Fichier introuvable' });
fs.unlinkSync(filePath);
const thumbPath = path.join(galleryDir, date, '.thumbnails', filename);
if (fs.existsSync(thumbPath)) fs.unlinkSync(thumbPath);
const dirPath = path.join(galleryDir, date);
if (fs.existsSync(dirPath) && fs.readdirSync(dirPath).filter((f) => f !== '.thumbnails').length === 0) {
const thumbDir = path.join(galleryDir, date, '.thumbnails');
if (fs.existsSync(thumbDir)) fs.rmdirSync(thumbDir);
fs.rmdirSync(dirPath);
}
res.json({ message: 'Photo supprimée' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
+231
View File
@@ -0,0 +1,231 @@
const pool = require('../config/db');
const crypto = require('crypto');
const { sendInvitationEmail } = require('../config/email');
const { logActivity } = require('../services/activityLog');
exports.list = async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT i.*, u.prenom AS invite_par_prenom, u.nom AS invite_par_nom
FROM invites i
LEFT JOIN users u ON u.id = i.invite_par_id
WHERE i.statut_candidature IS NULL OR i.statut_candidature != 'accepte'
ORDER BY i.date_club DESC, i.created_at DESC
`);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.getById = async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM invites WHERE id = ?', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Not found' });
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.search = async (req, res) => {
try {
const { q } = req.query;
if (!q || q.length < 2) return res.json([]);
const [rows] = await pool.query(`
SELECT prenom, nom, societe, activite, telephone, email, site_web,
MAX(visit_number) AS visit_number, COUNT(*) AS total_visits
FROM invites
WHERE CONCAT(prenom, ' ', nom) LIKE ?
GROUP BY prenom, nom, societe, activite, telephone, email, site_web
ORDER BY total_visits DESC LIMIT 20
`, [`%${q}%`]);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
const BLOCKED_DATES = ['2026-10-20', '2026-12-15'];
exports.create = async (req, res) => {
try {
const { prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club } = req.body;
if (!prenom || !nom || !telephone || !email || !date_club) {
return res.status(400).json({ error: 'Tous les champs sauf commentaires sont requis' });
}
if (BLOCKED_DATES.includes(date_club)) {
return res.status(403).json({ error: "Cette date n'accepte pas de nouveaux invités" });
}
const invite_par_id = req.user.id;
// Determine visit_number
const [existing] = await pool.query(
'SELECT MAX(visit_number) AS max_vn FROM invites WHERE prenom = ? AND nom = ?',
[prenom, nom]
);
const visit_number = (existing[0].max_vn || 0) + 1;
// Only admin can add 3rd+ visit
if (visit_number >= 3 && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Seul un administrateur peut ajouter une 3e visite ou plus' });
}
const [result] = await pool.query(
`INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[prenom, nom, societe || null, activite || null, telephone, email, site_web || null, commentaires || null, date_club, invite_par_id, visit_number]
);
logActivity(req, 'invite_ajout', { prenom, nom });
res.status(201).json({ id: result.insertId, message: 'Guest added', visit_number });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.update = async (req, res) => {
try {
const guestId = req.params.id;
const [existing] = await pool.query('SELECT invite_par_id FROM invites WHERE id = ?', [guestId]);
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
if (existing[0].invite_par_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'You can only edit your own guests' });
}
const { prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club } = req.body;
if (BLOCKED_DATES.includes(date_club)) {
return res.status(403).json({ error: "Cette date n'accepte pas de nouveaux invités" });
}
await pool.query(
`UPDATE invites SET prenom=?, nom=?, societe=?, activite=?, telephone=?, email=?, site_web=?, commentaires=?, date_club=? WHERE id=?`,
[prenom, nom, societe || null, activite || null, telephone, email, site_web || null, commentaires || null, date_club, guestId]
);
logActivity(req, 'invite_modification', { guestId, prenom, nom });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.remove = async (req, res) => {
try {
const guestId = req.params.id;
const [existing] = await pool.query('SELECT invite_par_id FROM invites WHERE id = ?', [guestId]);
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
if (existing[0].invite_par_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Vous ne pouvez supprimer que vos propres invités' });
}
await pool.query('DELETE FROM invites WHERE id = ?', [guestId]);
logActivity(req, 'invite_suppression', { guestId });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.sendInvitation = async (req, res) => {
try {
const guestId = req.params.id;
const [guests] = await pool.query(
`SELECT i.*, u.prenom AS invite_par_prenom, u.nom AS invite_par_nom
FROM invites i
LEFT JOIN users u ON u.id = i.invite_par_id
WHERE i.id = ?`,
[guestId]
);
if (guests.length === 0) return res.status(404).json({ error: 'Invité non trouvé' });
const guest = guests[0];
if (guest.invite_par_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Vous ne pouvez envoyer que vos propres invitations' });
}
if (!guest.email) {
return res.status(400).json({ error: "Cet invité n'a pas d'adresse email" });
}
const token = crypto.randomBytes(32).toString('hex');
const baseUrl = process.env.PUBLIC_URL || 'https://dyna.secourit.fr';
const [inviter] = await pool.query('SELECT prenom, nom FROM users WHERE id = ?', [guest.invite_par_id]);
await sendInvitationEmail(guest, inviter[0], token, baseUrl);
await pool.query(
'UPDATE invites SET token=?, mail_envoye=1, invitation_sent_at=NOW(), statut="en_attente" WHERE id=?',
[token, guestId]
);
logActivity(req, 'invite_email_envoye', { guestId, email: guest.email });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Erreur lors de l'envoi de l'email" });
}
};
exports.candidature = async (req, res) => {
try {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
const guestId = req.params.id;
const { action } = req.body;
if (!['start', 'accept', 'reject'].includes(action)) {
return res.status(400).json({ error: 'Action invalide (start/accept/reject)' });
}
const [guests] = await pool.query('SELECT * FROM invites WHERE id = ?', [guestId]);
if (guests.length === 0) return res.status(404).json({ error: 'Invité non trouvé' });
const guest = guests[0];
if (action === 'start') {
await pool.query(
'UPDATE invites SET statut_candidature = ?, candidature_date = NOW() WHERE id = ?',
['candidature', guestId]
);
logActivity(req, 'candidature_demarrage', { guestId, prenom: guest.prenom, nom: guest.nom });
return res.json({ message: 'Candidature en cours', statut_candidature: 'candidature' });
}
if (action === 'reject') {
await pool.query(
'UPDATE invites SET statut_candidature = ? WHERE id = ?',
['refuse', guestId]
);
logActivity(req, 'candidature_refus', { guestId, prenom: guest.prenom, nom: guest.nom });
return res.json({ message: 'Candidature refusée', statut_candidature: 'refuse' });
}
if (action === 'accept') {
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash('Bienvenue2026', 10);
const [result] = await pool.query(
`INSERT INTO users (prenom, nom, societe, activite, telephone, email, site_web, role, password_hash, date_adhesion, invite_par_id, actif)
VALUES (?, ?, ?, ?, ?, ?, ?, 'member', ?, CURDATE(), ?, 1)`,
[guest.prenom, guest.nom, guest.societe || null, guest.activite || null,
guest.telephone || null, guest.email || null, guest.site_web || null,
hash, guest.invite_par_id]
);
await pool.query('DELETE FROM invites WHERE id = ?', [guestId]);
logActivity(req, 'candidature_acceptee', { guestId, prenom: guest.prenom, nom: guest.nom });
return res.json({ message: 'Membre créé avec succès', user_id: result.insertId, defaultPassword: 'Bienvenue2026' });
}
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') {
return res.status(400).json({ error: 'Cet email est déjà utilisé par un membre existant' });
}
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.nextEventGuestCount = async (req, res) => {
try {
const [events] = await pool.query(
'SELECT date FROM evenements WHERE date >= CURDATE() ORDER BY date ASC LIMIT 1'
);
if (events.length === 0) return res.json({ count: 0, next_event_date: null });
const nextDate = events[0].date;
const [rows] = await pool.query(
'SELECT COUNT(*) AS count FROM invites WHERE date_club = ?',
[nextDate]
);
res.json({ count: rows[0].count, next_event_date: nextDate });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
+61
View File
@@ -0,0 +1,61 @@
const pool = require('../config/db');
const { logActivity } = require('../services/activityLog');
exports.list = async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT mh.*, u.prenom, u.nom, u.societe AS membre_societe
FROM mises_en_honneur mh
LEFT JOIN users u ON u.id = mh.membre_id
ORDER BY mh.date ASC
`);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.create = async (req, res) => {
try {
const { date, membre_id, societe, theme_sujet } = req.body;
if (!date || !theme_sujet) {
return res.status(400).json({ error: 'Date and theme required' });
}
const [result] = await pool.query(
'INSERT INTO mises_en_honneur (date, membre_id, societe, theme_sujet) VALUES (?, ?, ?, ?)',
[date, membre_id || null, societe || null, theme_sujet]
);
res.status(201).json({ id: result.insertId, message: 'Honor added' });
logActivity(req, 'honneur_ajout', { date, membre_id, theme_sujet });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.update = async (req, res) => {
try {
const { date, membre_id, societe, theme_sujet } = req.body;
await pool.query(
'UPDATE mises_en_honneur SET date = ?, membre_id = ?, societe = ?, theme_sujet = ? WHERE id = ?',
[date, membre_id || null, societe || null, theme_sujet, req.params.id]
);
res.json({ message: 'Updated' });
logActivity(req, 'honneur_modification', { id: req.params.id, date, membre_id, theme_sujet });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.remove = async (req, res) => {
try {
await pool.query('DELETE FROM mises_en_honneur WHERE id = ?', [req.params.id]);
res.json({ message: 'Deleted' });
logActivity(req, 'honneur_suppression', { id: req.params.id });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
+185
View File
@@ -0,0 +1,185 @@
const pool = require('../config/db');
const path = require('path');
const fs = require('fs');
exports.showPage = async (req, res) => {
try {
const { token } = req.params;
const reponse = req.query.reponse || null;
const [rows] = await pool.query(
`SELECT i.*, u.prenom AS invite_par_prenom, u.nom AS invite_par_nom, u.societe AS invite_par_societe
FROM invites i
LEFT JOIN users u ON u.id = i.invite_par_id
WHERE i.token = ?`,
[token]
);
if (rows.length === 0) {
return sendErrorPage(res, 'Lien invalide ou expiré.');
}
const guest = rows[0];
if (reponse && ['acceptee', 'refusee'].includes(reponse)) {
await pool.query('UPDATE invites SET statut=? WHERE id=?', [reponse, guest.id]);
guest.statut = reponse;
}
const dateStr = guest.date_club
? new Date(guest.date_club).toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })
: '';
const statutLabels = { en_attente: 'En attente', acceptee: 'Acceptée', refusee: 'Refusée' };
const statutColors = { en_attente: '#f59e0b', acceptee: '#16a34a', refusee: '#dc2626' };
const html = `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Invitation Dyna Biz</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:20px;min-height:100vh;display:flex;align-items:center;justify-content:center}
.card{background:#fff;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,.08);max-width:480px;width:100%;overflow:hidden}
.header{background:#1e3a5f;padding:24px;text-align:center}
.header h1{color:#fff;font-size:20px;margin:0}
.body{padding:28px 24px}
.body h2{font-size:18px;color:#333;margin-bottom:16px}
.body p{font-size:15px;color:#555;margin-bottom:12px;line-height:1.5}
.label{color:#888;font-size:13px}
.statut{display:inline-block;padding:4px 12px;border-radius:20px;font-size:14px;font-weight:bold;color:#fff;margin-top:4px}
.btn-group{display:flex;gap:12px;margin:24px 0;flex-wrap:wrap}
.btn{flex:1;min-width:120px;padding:14px 20px;border:none;border-radius:12px;font-size:16px;font-weight:bold;color:#fff;cursor:pointer;text-align:center;text-decoration:none;display:inline-block;transition:opacity .2s}
.btn:active{opacity:.8}
.btn-accept{background:#16a34a}
.btn-refuse{background:#dc2626}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn-upload{background:#1e3a5f;color:#fff;padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-size:14px;margin-top:8px}
.file-info{font-size:13px;color:#888;margin-top:6px}
.msg{background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:16px;text-align:center;margin:16px 0}
.msg.error{background:#fef2f2;border-color:#fecaca}
.msg.info{background:#eff6ff;border-color:#bfdbfe}
.photo-preview{max-width:200px;max-height:200px;border-radius:12px;margin-top:12px;display:block}
.link{color:#1e3a5f;text-decoration:underline;font-size:14px}
.hidden{display:none}
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>Dyna Biz Coignieres</h1>
</div>
<div class="body">
<h2>Bonjour ${guest.prenom} ${guest.nom} !</h2>
<p><strong>${guest.invite_par_prenom} ${guest.invite_par_nom}</strong>${guest.invite_par_societe ? ' (' + guest.invite_par_societe + ')' : ''} vous invite au club Dyna Biz Coignieres.</p>
${guest.date_club ? '<p><strong>Date :</strong> ' + dateStr + '</p>' : ''}
<p style="margin-top:16px;"><span class="label">Statut actuel :</span><br><span class="statut" style="background:${statutColors[guest.statut]}">${statutLabels[guest.statut]}</span></p>
${guest.statut === 'en_attente' ? `
<div class="btn-group">
<a href="/invitation/${token}?reponse=acceptee" class="btn btn-accept">Accepter</a>
<a href="/invitation/${token}?reponse=refusee" class="btn btn-refuse">Refuser</a>
</div>
` : `
<div class="msg${guest.statut === 'acceptee' ? '' : ' error'}">
${guest.statut === 'acceptee' ? 'Vous avez accepté l\'invitation. Merci !' : 'Vous avez refusé l\'invitation.'}
</div>
`}
${guest.statut === 'acceptee' ? `
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0">
<h2>Ajouter une photo</h2>
<p>Vous pouvez ajouter une photo pour que les membres puissent vous reconnaître.</p>
<img src="${guest.photo_url || ''}" alt="Votre photo" class="photo-preview" style="${guest.photo_url ? '' : 'display:none'}">
<form id="photoForm" enctype="multipart/form-data">
<input type="file" name="photo" accept="image/*" capture="environment" id="photoInput" style="display:block;margin:12px 0;font-size:14px">
<button type="submit" class="btn-upload" id="uploadBtn">Envoyer la photo</button>
<p class="file-info" id="uploadMsg"></p>
</form>
<script>
document.getElementById('photoForm').addEventListener('submit', async function(e) {
e.preventDefault();
const input = document.getElementById('photoInput');
const btn = document.getElementById('uploadBtn');
const msg = document.getElementById('uploadMsg');
if (!input.files.length) { msg.textContent = 'Veuillez sélectionner une photo.'; return; }
btn.disabled = true; btn.textContent = 'Envoi...';
const fd = new FormData();
fd.append('photo', input.files[0]);
try {
const r = await fetch('/api/public/invitation/${token}/photo', { method: 'POST', body: fd });
if (!r.ok) throw new Error('Erreur');
const data = await r.json();
msg.textContent = 'Photo envoyée avec succès !';
msg.style.color = '#16a34a';
if (data.photo_url) {
const img = document.querySelector('.photo-preview');
if (img) { img.src = data.photo_url + '?t=' + Date.now(); img.style.display = 'block'; }
}
input.value = '';
} catch(err) {
msg.textContent = 'Erreur lors de l\\'envoi.';
msg.style.color = '#dc2626';
} finally {
btn.disabled = false; btn.textContent = 'Envoyer la photo';
}
});
</script>
` : ''}
</div>
</div>
</body>
</html>`;
res.send(html);
} catch (err) {
console.error(err);
sendErrorPage(res, 'Une erreur est survenue.');
}
};
exports.respond = async (req, res) => {
try {
const { token } = req.params;
const { reponse } = req.body;
if (!['acceptee', 'refusee'].includes(reponse)) {
return res.status(400).json({ error: 'Réponse invalide' });
}
const [rows] = await pool.query('SELECT id FROM invites WHERE token = ?', [token]);
if (rows.length === 0) return res.status(404).json({ error: 'Lien invalide' });
await pool.query('UPDATE invites SET statut=? WHERE id=?', [reponse, rows[0].id]);
res.json({ message: 'Réponse enregistrée' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.uploadPhoto = async (req, res) => {
try {
const { token } = req.params;
const [rows] = await pool.query('SELECT id, photo_url FROM invites WHERE token = ?', [token]);
if (rows.length === 0) return res.status(404).json({ error: 'Lien invalide' });
if (!req.file) return res.status(400).json({ error: 'Fichier requis' });
if (rows[0].photo_url) {
const oldPath = path.join(__dirname, '..', rows[0].photo_url);
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
}
const photoUrl = '/uploads/' + req.file.filename;
await pool.query('UPDATE invites SET photo_url=? WHERE id=?', [photoUrl, rows[0].id]);
res.json({ message: 'Photo enregistrée', photo_url: photoUrl });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
function sendErrorPage(res, msg) {
res.status(404).send(`<!DOCTYPE html>
<html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Invitation Dyna Biz</title>
<style>body{font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f3f4f6;padding:20px}
.card{background:#fff;border-radius:16px;padding:32px;text-align:center;max-width:400px;box-shadow:0 2px 12px rgba(0,0,0,.08)}
.card h2{color:#333;margin:0 0 8px}.card p{color:#888;font-size:15px}</style>
</head><body><div class="card"><h2>Oups !</h2><p>${msg}</p></div></body></html>`);
}
+171
View File
@@ -0,0 +1,171 @@
const pool = require('../config/db');
const { logActivity } = require('../services/activityLog');
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');
exports.list = async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT u.id, u.prenom, u.nom, u.societe, u.activite, u.email, u.telephone,
u.photo_url, u.merged_display_name, u.site_web, u.role, u.date_adhesion, u.date_echeance,
u.invite_par_id, ip.prenom AS invite_par_prenom, ip.nom AS invite_par_nom,
u.date_postulation, u.reception_connu, u.reception_inconnu,
rc.prenom AS reception_connu_prenom, rc.nom AS reception_connu_nom,
ri.prenom AS reception_inconnu_prenom, ri.nom AS reception_inconnu_nom,
u.date_entretien_postulation, u.date_demande_whatsapp,
u.fonction, u.fonction_emoji, u.kdrive_link
FROM users u
LEFT JOIN users ip ON ip.id = u.invite_par_id
LEFT JOIN users rc ON rc.id = u.reception_connu
LEFT JOIN users ri ON ri.id = u.reception_inconnu
WHERE u.actif = 1
ORDER BY u.prenom ASC
`);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.getById = async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT u.*,
ip.prenom AS invite_par_prenom, ip.nom AS invite_par_nom,
rc.prenom AS reception_connu_prenom, rc.nom AS reception_connu_nom,
ri.prenom AS reception_inconnu_prenom, ri.nom AS reception_inconnu_nom
FROM users u
LEFT JOIN users ip ON ip.id = u.invite_par_id
LEFT JOIN users rc ON rc.id = u.reception_connu
LEFT JOIN users ri ON ri.id = u.reception_inconnu
WHERE u.id = ?
`, [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Not found' });
const { password_hash, ...member } = rows[0];
res.json(member);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.update = async (req, res) => {
try {
const memberId = req.params.id;
if (Number(memberId) !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
const { prenom, nom, societe, activite, email, telephone, site_web,
date_adhesion, date_echeance,
invite_par_id, date_postulation, reception_connu, reception_inconnu,
date_entretien_postulation, date_demande_whatsapp,
fonction, fonction_emoji, kdrive_link, merged_display_name } = req.body;
if (!prenom || !nom) return res.status(400).json({ error: 'Prénom et nom requis' });
const isAdmin = req.user.role === 'admin';
const adminFields = isAdmin ? {
date_adhesion: date_adhesion || null,
date_echeance: date_echeance || null,
invite_par_id: invite_par_id || null,
date_postulation: date_postulation || null,
reception_connu: reception_connu || null,
reception_inconnu: reception_inconnu || null,
date_entretien_postulation: date_entretien_postulation || null,
date_demande_whatsapp: date_demande_whatsapp || null,
fonction: fonction || null,
fonction_emoji: fonction_emoji || null,
kdrive_link: kdrive_link || null,
merged_display_name: merged_display_name || null,
} : {};
await pool.query(
`UPDATE users SET prenom=?, nom=?, societe=?, activite=?, email=?, telephone=?, site_web=?,
date_adhesion=?, date_echeance=?, invite_par_id=?, date_postulation=?,
reception_connu=?, reception_inconnu=?, date_entretien_postulation=?,
date_demande_whatsapp=?, fonction=?, fonction_emoji=?, kdrive_link=?, merged_display_name=? WHERE id=?`,
[prenom, nom, societe || null, activite || null, email || null, telephone || null, site_web || null,
adminFields.date_adhesion, adminFields.date_echeance,
adminFields.invite_par_id, adminFields.date_postulation,
adminFields.reception_connu, adminFields.reception_inconnu,
adminFields.date_entretien_postulation, adminFields.date_demande_whatsapp,
adminFields.fonction, adminFields.fonction_emoji, adminFields.kdrive_link,
adminFields.merged_display_name,
memberId]
);
logActivity(req, 'membre_modification', { memberId });
res.json({ message: 'Mis à jour' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Erreur serveur' });
}
};
exports.uploadPhoto = async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
const filePath = req.file.path;
const ext = path.extname(filePath);
const jpgPath = ext === '.jpg' ? filePath : filePath.replace(ext, '.jpg');
try {
await sharp(filePath).resize(400, 400, { fit: 'cover', position: 'centre' }).jpeg({ quality: 85 }).toFile(jpgPath);
if (jpgPath !== filePath) fs.unlinkSync(filePath);
} catch (sharpErr) {
console.error('Sharp error, fallback:', sharpErr);
if (jpgPath !== filePath) fs.renameSync(filePath, jpgPath);
}
const finalUrl = `/uploads/${path.basename(jpgPath)}`;
const [existing] = await pool.query('SELECT photo_url FROM users WHERE id = ?', [req.user.id]);
if (existing[0]?.photo_url) {
const oldRelativePath = existing[0].photo_url.replace(/^\//, '');
const oldPath = path.join(__dirname, '..', oldRelativePath);
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
}
await pool.query('UPDATE users SET photo_url = ? WHERE id = ?', [finalUrl, req.user.id]);
logActivity(req, 'photo_changement');
res.json({ photo_url: finalUrl });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.create = async (req, res) => {
try {
const { prenom, nom, societe, activite, email, telephone, role } = req.body;
if (!prenom || !nom || !email) {
return res.status(400).json({ error: 'Prénom, nom et email requis' });
}
const defaultPassword = 'Bienvenue2026';
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(defaultPassword, 10);
const [result] = await pool.query(
'INSERT INTO users (prenom, nom, societe, activite, email, telephone, role, password_hash, date_adhesion, actif) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURDATE(), 1)',
[prenom, nom, societe || null, activite || null, email, telephone || null, role || 'member', hash]
);
res.status(201).json({
id: result.insertId,
message: 'Membre créé',
defaultPassword: 'Bienvenue2026',
});
logActivity(req, 'membre_ajout', { prenom, nom });
} catch (err) {
if (err.code === 'ER_DUP_ENTRY') {
return res.status(400).json({ error: 'Cet email est déjà utilisé' });
}
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.remove = async (req, res) => {
try {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
const memberId = req.params.id;
if (Number(memberId) === req.user.id) return res.status(400).json({ error: 'Vous ne pouvez pas vous supprimer vous-même' });
await pool.query('UPDATE users SET actif = 0 WHERE id = ?', [memberId]);
logActivity(req, 'membre_desactivation', { memberId });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
@@ -0,0 +1,112 @@
const pool = require('../config/db');
const { logActivity } = require('../services/activityLog');
exports.list = async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT r.*,
m.prenom AS membre_prenom, m.nom AS membre_nom,
d.prenom AS dest_prenom, d.nom AS dest_nom
FROM recommandations r
LEFT JOIN users m ON m.id = r.membre_id
LEFT JOIN users d ON d.id = r.destinataire_id
ORDER BY r.date DESC
`);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.getById = async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM recommandations WHERE id = ?', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Not found' });
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.create = async (req, res) => {
try {
const { date, destinataire_id, nom_prospect, type, transformee, sans_suite, ca_realise } = req.body;
if (!date || !nom_prospect || !type) {
return res.status(400).json({ error: 'Date, prospect name and type required' });
}
const membre_id = req.user.id;
const [result] = await pool.query(
`INSERT INTO recommandations (date, membre_id, destinataire_id, nom_prospect, type, transformee, sans_suite, ca_realise)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[date, membre_id, destinataire_id || null, nom_prospect, type, transformee ? 1 : 0, sans_suite ? 1 : 0, ca_realise || null]
);
logActivity(req, 'recommandation_ajout', { nom_prospect, type });
res.status(201).json({ id: result.insertId, message: 'Recommendation added' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.update = async (req, res) => {
try {
const recId = req.params.id;
const [existing] = await pool.query('SELECT membre_id FROM recommandations WHERE id = ?', [recId]);
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
if (existing[0].membre_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'You can only edit your own recommendations' });
}
const { date, destinataire_id, nom_prospect, type, transformee, sans_suite, ca_realise } = req.body;
await pool.query(
`UPDATE recommandations SET date=?, destinataire_id=?, nom_prospect=?, type=?, transformee=?, sans_suite=?, ca_realise=? WHERE id=?`,
[date, destinataire_id || null, nom_prospect, type, transformee ? 1 : 0, sans_suite ? 1 : 0, ca_realise || null, recId]
);
logActivity(req, 'recommandation_modification', { recId, nom_prospect });
res.json({ message: 'Updated' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.remove = async (req, res) => {
try {
const recId = req.params.id;
const [existing] = await pool.query('SELECT membre_id FROM recommandations WHERE id = ?', [recId]);
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
if (existing[0].membre_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Vous ne pouvez supprimer que vos propres recommandations' });
}
await pool.query('DELETE FROM recommandations WHERE id = ?', [recId]);
logActivity(req, 'recommandation_suppression', { recId });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.setStatus = async (req, res) => {
try {
const recId = req.params.id;
const [existing] = await pool.query('SELECT membre_id FROM recommandations WHERE id = ?', [recId]);
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
if (existing[0].membre_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Vous ne pouvez modifier que vos propres recommandations' });
}
const { status, ca_realise } = req.body;
if (status === 'transformee') {
await pool.query('UPDATE recommandations SET transformee=1, sans_suite=0, ca_realise=? WHERE id=?', [ca_realise || null, recId]);
} else if (status === 'sans_suite') {
await pool.query('UPDATE recommandations SET transformee=0, sans_suite=1, ca_realise=NULL WHERE id=?', [recId]);
} else {
await pool.query('UPDATE recommandations SET transformee=0, sans_suite=0 WHERE id=?', [recId]);
}
logActivity(req, 'recommandation_statut', { recId, status });
res.json({ message: 'Status updated' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
+203
View File
@@ -0,0 +1,203 @@
const pool = require('../config/db');
exports.dashboard = async (req, res) => {
try {
const { filter } = req.query;
let dateCondition;
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
if (filter === 'year') {
dateCondition = `r.date >= '${year}-01-01'`;
} else if (filter === 'month') {
dateCondition = `r.date >= '${year}-${month}-01'`;
} else {
dateCondition = `r.date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)`;
}
const nextEventPromise = pool.query(`
SELECT e.date, e.nom_evenement,
(SELECT COUNT(*) FROM invites WHERE date_club = e.date) AS invites,
(SELECT SUM(statut = 'acceptee') FROM invites WHERE date_club = e.date) AS acceptees,
(SELECT SUM(statut = 'refusee') FROM invites WHERE date_club = e.date) AS refusees,
(SELECT SUM(visit_number >= 2) FROM invites WHERE date_club = e.date) AS secondes_visites
FROM evenements e
WHERE e.nom_evenement = 'Club d''Affaires' AND e.date >= CURDATE()
ORDER BY e.date ASC LIMIT 1
`);
const honorsPromise = pool.query(
"SELECT mh.*, u.prenom, u.nom, u.societe AS membre_societe FROM mises_en_honneur mh LEFT JOIN users u ON u.id = mh.membre_id WHERE mh.date >= CURDATE() ORDER BY mh.date ASC LIMIT 5"
);
const eventsPromise = pool.query(
'SELECT * FROM evenements WHERE date >= CURDATE() ORDER BY date ASC LIMIT 3'
);
const recsPromise = pool.query(`
SELECT r.id, r.date, r.nom_prospect, r.type, r.transformee, r.ca_realise,
m.prenom AS membre_prenom, m.nom AS membre_nom,
d.prenom AS dest_prenom, d.nom AS dest_nom
FROM recommandations r
LEFT JOIN users m ON m.id = r.membre_id
LEFT JOIN users d ON d.id = r.destinataire_id
WHERE ${dateCondition}
ORDER BY r.date DESC
`);
const countPromise = pool.query(`
SELECT
COUNT(*) AS total,
SUM(CASE WHEN r.type='donnee' THEN 1 ELSE 0 END) AS donnees,
SUM(CASE WHEN r.type='recue' THEN 1 ELSE 0 END) AS recues,
SUM(r.transformee) AS transformees,
SUM(COALESCE(r.ca_realise,0)) AS ca_total
FROM recommandations r
WHERE ${dateCondition}
`);
const memberRecsPromise = pool.query(`
SELECT
u.id, u.prenom, u.nom, u.societe,
COUNT(r.id) AS total_recs,
SUM(r.transformee) AS total_transformees,
SUM(COALESCE(r.ca_realise,0)) AS total_ca
FROM users u
LEFT JOIN recommandations r ON r.membre_id = u.id AND ${dateCondition}
WHERE u.actif = 1
GROUP BY u.id
ORDER BY total_recs DESC
`);
const guestCountPromise = pool.query(`
SELECT i.invite_par_id, u.prenom, u.nom, COUNT(i.id) AS total_invites
FROM invites i
JOIN users u ON u.id = i.invite_par_id
WHERE i.date_club >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY i.invite_par_id, u.prenom, u.nom
ORDER BY total_invites DESC
`);
const echeancePromise = pool.query(
"SELECT id, prenom, nom, societe, date_echeance FROM users WHERE actif = 1 AND date_echeance IS NOT NULL AND date_echeance BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY) ORDER BY date_echeance ASC"
);
const [nextEvent] = await nextEventPromise;
const [honors] = await honorsPromise;
const [events] = await eventsPromise;
const [recs] = await recsPromise;
const [counts] = await countPromise;
const [memberRecs] = await memberRecsPromise;
const [guestCounts] = await guestCountPromise;
const guestMetricsPromise = pool.query(`
SELECT
COUNT(*) AS total_invites,
SUM(CASE WHEN statut = 'acceptee' THEN 1 ELSE 0 END) AS acceptees,
SUM(CASE WHEN statut = 'refusee' THEN 1 ELSE 0 END) AS refusees,
SUM(CASE WHEN visit_number >= 2 THEN 1 ELSE 0 END) AS secondes_visites
FROM invites
`);
const [echeances] = await echeancePromise;
const [guestMetricsRows] = await guestMetricsPromise;
let top_absences = [];
if (req.user.role === 'admin') {
const [absRows] = await pool.query(`
SELECT u.id, u.prenom, u.nom, COUNT(a.id) AS total_absences
FROM users u
LEFT JOIN absences a ON a.membre_id = u.id
WHERE u.actif = 1
GROUP BY u.id
HAVING total_absences > 0
ORDER BY total_absences DESC
LIMIT 5
`);
top_absences = absRows;
}
let top_conversion = [];
const [convRows] = await pool.query(`
SELECT u.id, u.prenom, u.nom, COUNT(m.id) AS total_convertis
FROM users u
JOIN users m ON m.invite_par_id = u.id
WHERE u.actif = 1
GROUP BY u.id
ORDER BY total_convertis DESC
LIMIT 15
`);
top_conversion = convRows;
const [candRows] = await pool.query(
'SELECT id, prenom, nom, societe, created_at FROM invites WHERE statut_candidature = ? ORDER BY created_at DESC',
['candidature']
);
const candidatures = candRows;
const total = Number(counts[0].total) || 0;
const transformees = Number(counts[0].transformees) || 0;
const tauxTransformation = total > 0 ? ((transformees / total) * 100).toFixed(1) : 0;
const topDonnees = [...memberRecs].filter(m => m.total_recs > 0).sort((a, b) => b.total_recs - a.total_recs).slice(0, 15);
const topInvites = [...guestCounts].sort((a, b) => b.total_invites - a.total_invites).slice(0, 15);
const flopDonnees = [...memberRecs].sort((a, b) => a.total_recs - b.total_recs).slice(0, 5);
res.json({
prochaine_seance: nextEvent.length > 0 ? {
date: nextEvent[0].date,
nom: nextEvent[0].nom_evenement,
invites: Number(nextEvent[0].invites) || 0,
acceptees: Number(nextEvent[0].acceptees) || 0,
refusees: Number(nextEvent[0].refusees) || 0,
secondes_visites: Number(nextEvent[0].secondes_visites) || 0,
} : null,
counts: {
total,
donnees: Number(counts[0].donnees) || 0,
recues: Number(counts[0].recues) || 0,
transformees,
ca_total: Number(counts[0].ca_total) || 0,
taux_transformation: tauxTransformation,
},
top_donnees: topDonnees,
top_invites: topInvites,
flop_donnees: flopDonnees,
prochaines_honors: honors,
prochains_evenements: events,
alertes_echeance: echeances,
guest_metrics: {
total_invites: Number(guestMetricsRows[0].total_invites) || 0,
acceptees: Number(guestMetricsRows[0].acceptees) || 0,
refusees: Number(guestMetricsRows[0].refusees) || 0,
secondes_visites: Number(guestMetricsRows[0].secondes_visites) || 0,
},
top_absences,
top_conversion,
candidatures,
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.memberStats = async (req, res) => {
try {
const userId = req.params.id || req.user.id;
const [recs] = await pool.query(`
SELECT r.*, d.prenom AS dest_prenom, d.nom AS dest_nom
FROM recommandations r
LEFT JOIN users d ON d.id = r.destinataire_id
WHERE r.membre_id = ?
ORDER BY r.date DESC
`, [userId]);
const [guests] = await pool.query(
'SELECT * FROM invites WHERE invite_par_id = ? ORDER BY created_at DESC',
[userId]
);
res.json({ recommandations: recs, invites: guests });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};