Initial commit: Club DynaSQY
This commit is contained in:
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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>`);
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user