Initial commit: Club DynaSQY
@@ -0,0 +1,15 @@
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=club_dynasqy
|
||||
DB_PASSWORD=change_this_password
|
||||
DB_NAME=club_dynasqy
|
||||
JWT_SECRET=change_this_jwt_secret_key_64chars_min
|
||||
PORT=3000
|
||||
|
||||
# SMTP (Infomaniak)
|
||||
SMTP_HOST=mail.infomaniak.com
|
||||
SMTP_PORT=465
|
||||
SMTP_USER=invitation@votredomaine.com
|
||||
SMTP_PASS=your_smtp_password
|
||||
EMAIL_FROM=invitation@votredomaine.com
|
||||
PUBLIC_URL=https://dyna.secourit.fr
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.env
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
RUN apk add --no-cache libreoffice
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install --production
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -0,0 +1,16 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT, 10) || 3306,
|
||||
user: process.env.DB_USER || 'club_dynasqy',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'club_dynasqy',
|
||||
charset: 'utf8mb4',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
|
||||
module.exports = pool;
|
||||
@@ -0,0 +1,73 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'mail.infomaniak.com',
|
||||
port: parseInt(process.env.SMTP_PORT, 10) || 465,
|
||||
secure: parseInt(process.env.SMTP_PORT, 10) === 465 || true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
async function sendInvitationEmail(guest, inviter, token, baseUrl) {
|
||||
const link = `${baseUrl}/invitation/${token}`;
|
||||
const dateStr = guest.date_club
|
||||
? new Date(guest.date_club).toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })
|
||||
: 'à venir';
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
|
||||
<body style="margin:0;padding:0;background:#f3f4f6;font-family:Arial,Helvetica,sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr><td style="padding:40px 20px;">
|
||||
<table role="presentation" width="480" align="center" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.08);">
|
||||
<tr><td style="background:#1e3a5f;padding:24px;text-align:center;">
|
||||
<h1 style="color:#fff;margin:0;font-size:22px;">Dyna Biz Coignieres</h1>
|
||||
</td></tr>
|
||||
<tr><td style="padding:32px 28px;">
|
||||
<p style="margin:0 0 16px;font-size:16px;color:#333;">Bonjour <strong>${guest.prenom} ${guest.nom}</strong>,</p>
|
||||
<p style="margin:0 0 16px;font-size:15px;color:#555;">
|
||||
${inviter.prenom} ${inviter.nom} de Dyna Biz Coignieres vous invite à participer à notre prochain événement réseau.
|
||||
</p>
|
||||
${guest.date_club ? `<p style="margin:0 0 16px;font-size:15px;color:#555;"><strong>Date :</strong> ${dateStr}</p>` : ''}
|
||||
<p style="margin:0 0 16px;font-size:15px;color:#555;"><strong>Horaires :</strong> 10h45 - 13h00</p>
|
||||
<p style="margin:0 0 16px;font-size:15px;color:#555;"><strong>Lieu :</strong> 7 Av. Roger Hennequin, 78190 Trappes</p>
|
||||
<p style="margin:0 0 24px;font-size:15px;color:#555;">Merci de nous confirmer votre présence en cliquant sur l'un des boutons ci-dessous :</p>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td align="center" style="padding:0 4px 12px;">
|
||||
<a href="${link}?reponse=acceptee" style="display:inline-block;padding:14px 40px;background:#16a34a;color:#fff;text-decoration:none;border-radius:12px;font-size:16px;font-weight:bold;">J'accepte</a>
|
||||
</td>
|
||||
<td align="center" style="padding:0 4px 12px;">
|
||||
<a href="${link}?reponse=refusee" style="display:inline-block;padding:14px 40px;background:#dc2626;color:#fff;text-decoration:none;border-radius:12px;font-size:16px;font-weight:bold;">Je refuse</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:20px 0 0;font-size:13px;color:#888;text-align:center;">
|
||||
Ou rendez-vous sur :<br><a href="${link}" style="color:#1e3a5f;">${link}</a>
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: `"Dyna Biz Coignieres" <${process.env.EMAIL_FROM}>`,
|
||||
to: guest.email,
|
||||
subject: `Invitation - Dyna Biz Coignieres ${guest.date_club ? new Date(guest.date_club).toLocaleDateString('fr-FR') : ''}`,
|
||||
html,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Email send error:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { transporter, sendInvitationEmail };
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
const webPush = require('web-push');
|
||||
const keys = webPush.generateVAPIDKeys();
|
||||
console.log(keys.publicKey);
|
||||
console.log(keys.privateKey);
|
||||
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 3.4 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 526 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,25 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
function authenticate(req, res, next) {
|
||||
const header = req.headers.authorization;
|
||||
if (!header || !header.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Token required' });
|
||||
}
|
||||
const token = header.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
}
|
||||
|
||||
function adminOnly(req, res, next) {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { authenticate, adminOnly };
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "club-dynasqy-backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.12.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"sharp": "^0.33.5",
|
||||
"web-push": "^3.6.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const { Router } = require('express');
|
||||
const { listByMember, create, remove, stats } = require('../controllers/absenceController');
|
||||
const { authenticate, adminOnly } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/stats', authenticate, adminOnly, stats);
|
||||
router.get('/member/:id', authenticate, adminOnly, listByMember);
|
||||
router.post('/', authenticate, adminOnly, create);
|
||||
router.delete('/:id', authenticate, adminOnly, remove);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,8 @@
|
||||
const { Router } = require('express');
|
||||
const { getLogs } = require('../controllers/activityLogController');
|
||||
const { authenticate, adminOnly } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/', authenticate, adminOnly, getLogs);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,9 @@
|
||||
const { Router } = require('express');
|
||||
const { listUsers, updateUserRole } = require('../controllers/adminController');
|
||||
const { authenticate, adminOnly } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/users', authenticate, adminOnly, listUsers);
|
||||
router.put('/users/:id/role', authenticate, adminOnly, updateUserRole);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,11 @@
|
||||
const { Router } = require('express');
|
||||
const { login, me, changePassword } = require('../controllers/authController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.post('/login', login);
|
||||
router.get('/me', authenticate, me);
|
||||
|
||||
router.put('/password', authenticate, changePassword);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,13 @@
|
||||
const { Router } = require('express');
|
||||
const { list, create, update, remove, scrape, deletePast } = require('../controllers/eventController');
|
||||
const { authenticate, adminOnly } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/', authenticate, list);
|
||||
router.post('/', authenticate, adminOnly, create);
|
||||
router.post('/scrape', authenticate, adminOnly, scrape);
|
||||
router.delete('/past', authenticate, adminOnly, deletePast);
|
||||
router.put('/:id', authenticate, adminOnly, update);
|
||||
router.delete('/:id', authenticate, adminOnly, remove);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,8 @@
|
||||
const { Router } = require('express');
|
||||
const { exportCsv } = require('../controllers/exportController');
|
||||
const { authenticate, adminOnly } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/csv', authenticate, adminOnly, exportCsv);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,52 @@
|
||||
const { Router } = require('express');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const sharp = require('sharp');
|
||||
const { list, upload, deletePhoto } = require('../controllers/galleryController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const galleryDir = path.join(__dirname, '..', 'gallery');
|
||||
if (!fs.existsSync(galleryDir)) fs.mkdirSync(galleryDir, { recursive: true });
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, galleryDir),
|
||||
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname),
|
||||
});
|
||||
const uploadMulter = multer({ storage, limits: { fileSize: 20 * 1024 * 1024 } });
|
||||
|
||||
const router = Router();
|
||||
router.get('/', authenticate, list);
|
||||
router.post('/upload', authenticate, uploadMulter.array('photos', 20), upload);
|
||||
router.get('/thumbnail/:date/:filename', async (req, res) => {
|
||||
try {
|
||||
const { date, filename } = req.params;
|
||||
const filePath = path.join(galleryDir, date, filename);
|
||||
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Fichier introuvable' });
|
||||
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
|
||||
return res.sendFile(filePath);
|
||||
}
|
||||
|
||||
const thumbDir = path.join(galleryDir, date, '.thumbnails');
|
||||
if (!fs.existsSync(thumbDir)) fs.mkdirSync(thumbDir, { recursive: true });
|
||||
const thumbPath = path.join(thumbDir, filename);
|
||||
|
||||
if (!fs.existsSync(thumbPath)) {
|
||||
await sharp(filePath)
|
||||
.resize(300, 200, { fit: 'cover', position: 'centre' })
|
||||
.jpeg({ quality: 70 })
|
||||
.toFile(thumbPath);
|
||||
}
|
||||
|
||||
res.sendFile(thumbPath);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:date/:filename', authenticate, deletePhoto);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,16 @@
|
||||
const { Router } = require('express');
|
||||
const { list, getById, create, update, remove, search, sendInvitation, candidature, nextEventGuestCount } = require('../controllers/guestController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/', authenticate, list);
|
||||
router.get('/search', authenticate, search);
|
||||
router.get('/:id', authenticate, getById);
|
||||
router.post('/', authenticate, create);
|
||||
router.put('/:id', authenticate, update);
|
||||
router.delete('/:id', authenticate, remove);
|
||||
router.post('/:id/send-invitation', authenticate, sendInvitation);
|
||||
router.post('/:id/candidature', authenticate, candidature);
|
||||
router.get('/next-count', authenticate, nextEventGuestCount);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,11 @@
|
||||
const { Router } = require('express');
|
||||
const { list, create, update, remove } = require('../controllers/honorController');
|
||||
const { authenticate, adminOnly } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/', authenticate, list);
|
||||
router.post('/', authenticate, adminOnly, create);
|
||||
router.put('/:id', authenticate, adminOnly, update);
|
||||
router.delete('/:id', authenticate, adminOnly, remove);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,21 @@
|
||||
const { Router } = require('express');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const { showPage, respond, uploadPhoto } = require('../controllers/invitationController');
|
||||
|
||||
const router = Router();
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, path.join(__dirname, '..', 'uploads')),
|
||||
filename: (req, file, cb) => {
|
||||
const { token } = req.params;
|
||||
cb(null, `invite_${token}_${Date.now()}${path.extname(file.originalname)}`);
|
||||
},
|
||||
});
|
||||
const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 } });
|
||||
|
||||
router.get('/:token', showPage);
|
||||
router.post('/:token/respond', respond);
|
||||
router.post('/:token/photo', upload.single('photo'), uploadPhoto);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,100 @@
|
||||
const { Router } = require('express');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const pool = require('../config/db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const { logActivity } = require('../services/activityLog');
|
||||
|
||||
const slidesDir = path.join(__dirname, '..', 'member_slides');
|
||||
if (!fs.existsSync(slidesDir)) fs.mkdirSync(slidesDir, { recursive: true });
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, slidesDir),
|
||||
filename: (req, file, cb) => {
|
||||
const memberId = req.body.member_id && req.user.role === 'admin' ? req.body.member_id : req.user.id;
|
||||
const dateKey = req.body.date_key || '00000000';
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${memberId}_${dateKey}_${Date.now()}${ext}`);
|
||||
},
|
||||
});
|
||||
const uploadMulter = multer({ storage, limits: { fileSize: 300 * 1024 * 1024 } });
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/upload', authenticate, uploadMulter.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ error: 'Aucun fichier' });
|
||||
const { date_key } = req.body;
|
||||
if (!date_key) return res.status(400).json({ error: 'date_key requis' });
|
||||
const memberId = req.body.member_id && req.user.role === 'admin' ? req.body.member_id : req.user.id;
|
||||
|
||||
let filename = req.file.filename;
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
|
||||
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
|
||||
const videoExts = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv'];
|
||||
const allowedExts = [...imageExts, ...videoExts];
|
||||
if (!allowedExts.includes(ext)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
return res.status(400).json({ error: 'Seules les images et vidéos sont acceptées' });
|
||||
}
|
||||
|
||||
const [result] = await pool.query(
|
||||
'INSERT INTO member_slides (member_id, filename, original_name, date_key) VALUES (?, ?, ?, ?)',
|
||||
[memberId, filename, req.file.originalname, date_key]
|
||||
);
|
||||
res.json({ success: true, id: result.insertId, filename });
|
||||
logActivity(req, 'slide_upload', { memberId, date_key, original_name: req.file.originalname });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur upload' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:memberId', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { memberId } = req.params;
|
||||
if (Number(memberId) !== req.user.id && req.user.role !== 'admin')
|
||||
return res.status(403).json({ error: 'Interdit' });
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, filename, original_name, date_key, created_at FROM member_slides WHERE member_id = ? ORDER BY created_at DESC',
|
||||
[memberId]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM member_slides WHERE id = ?', [req.params.id]);
|
||||
if (rows.length === 0) return res.status(404).json({ error: 'Introuvable' });
|
||||
const slide = rows[0];
|
||||
if (slide.member_id !== req.user.id && req.user.role !== 'admin')
|
||||
return res.status(403).json({ error: 'Interdit' });
|
||||
const filePath = path.join(slidesDir, slide.filename);
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
||||
await pool.query('DELETE FROM member_slides WHERE id = ?', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
logActivity(req, 'slide_suppression', { slideId: req.params.id, filename: slide.filename });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.use((err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(413).json({ error: 'Le fichier dépasse la limite autorisée (300 Mo maximum)' });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
if (err) return res.status(500).json({ error: 'Erreur serveur' });
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,22 @@
|
||||
const { Router } = require('express');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const { list, getById, update, uploadPhoto, create, remove } = require('../controllers/memberController');
|
||||
const { authenticate, adminOnly } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, 'uploads/'),
|
||||
filename: (req, file, cb) => cb(null, `user_${req.user.id}_${Date.now()}${path.extname(file.originalname)}`)
|
||||
});
|
||||
const upload = multer({ storage });
|
||||
|
||||
router.get('/', authenticate, list);
|
||||
router.get('/:id', authenticate, getById);
|
||||
router.put('/:id', authenticate, update);
|
||||
router.post('/upload-photo', authenticate, upload.single('photo'), uploadPhoto);
|
||||
router.post('/', authenticate, adminOnly, create);
|
||||
router.delete('/:id', authenticate, adminOnly, remove);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,97 @@
|
||||
const { Router } = require('express');
|
||||
const pool = require('../config/db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT n.id, n.title, n.body, n.created_at,
|
||||
u.prenom AS sender_prenom, u.nom AS sender_nom,
|
||||
nr.read_at, 'admin' AS source,
|
||||
NULL AS invite_id,
|
||||
NULL AS guest_name
|
||||
FROM notifications n
|
||||
JOIN notification_recipients nr ON nr.notification_id = n.id
|
||||
JOIN users u ON u.id = n.sender_id
|
||||
WHERE nr.user_id = ?
|
||||
UNION ALL
|
||||
SELECT -(i.id) AS id,
|
||||
CONCAT('Invitation en attente - ', i.prenom, ' ', i.nom) AS title,
|
||||
CONCAT('Vous n''avez pas encore envoy?? l''email d''invitation ?? ', i.prenom, ' ', i.nom) AS body,
|
||||
i.created_at,
|
||||
NULL AS sender_prenom, NULL AS sender_nom,
|
||||
NULL AS read_at,
|
||||
'pending_invite' AS source,
|
||||
i.id AS invite_id,
|
||||
CONCAT(i.prenom, ' ', i.nom) AS guest_name
|
||||
FROM invites i
|
||||
WHERE i.invite_par_id = ? AND (i.mail_envoye IS NULL OR i.mail_envoye = 0)
|
||||
ORDER BY created_at DESC`,
|
||||
[req.user.id, req.user.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/unread-count', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT COUNT(*) AS count FROM notification_recipients WHERE user_id = ? AND read_at IS NULL',
|
||||
[req.user.id]
|
||||
);
|
||||
const [inviteRows] = await pool.query(
|
||||
'SELECT COUNT(*) AS count FROM invites WHERE invite_par_id = ? AND (mail_envoye IS NULL OR mail_envoye = 0)',
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ count: Number(rows[0].count) + Number(inviteRows[0].count) });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/read', authenticate, async (req, res) => {
|
||||
try {
|
||||
await pool.query(
|
||||
'UPDATE notification_recipients SET read_at = NOW() WHERE notification_id = ? AND user_id = ?',
|
||||
[req.params.id, req.user.id]
|
||||
);
|
||||
res.json({ message: 'Marqu?? comme lu' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/read-all', authenticate, async (req, res) => {
|
||||
try {
|
||||
await pool.query(
|
||||
'UPDATE notification_recipients SET read_at = NOW() WHERE user_id = ? AND read_at IS NULL',
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ message: 'Tout marqu?? comme lu' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id/recipient', authenticate, async (req, res) => {
|
||||
try {
|
||||
await pool.query(
|
||||
'DELETE FROM notification_recipients WHERE notification_id = ? AND user_id = ?',
|
||||
[req.params.id, req.user.id]
|
||||
);
|
||||
res.json({ message: 'Supprim??' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,136 @@
|
||||
const { Router } = require('express');
|
||||
const pool = require('../config/db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/:date', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
|
||||
try {
|
||||
const { date } = req.params;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
return res.status(400).json({ error: 'Format de date invalide (AAAA-MM-JJ)' });
|
||||
}
|
||||
|
||||
const [absences] = await pool.query(`
|
||||
SELECT a.membre_id, a.prevenu, a.motif,
|
||||
a.remplace_par_type, a.remplace_par_id, a.remplace_par_guest_id,
|
||||
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 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.date_absence = ?
|
||||
`, [date]);
|
||||
|
||||
const [allMembers] = await pool.query(
|
||||
`SELECT id, prenom, nom, societe, activite, photo_url, merged_display_name, fonction, fonction_emoji,
|
||||
telephone, site_web, email
|
||||
FROM users WHERE actif = 1 ORDER BY fonction IS NULL, fonction, prenom`
|
||||
);
|
||||
|
||||
const absentWithoutReplacement = new Set(
|
||||
absences.filter(a => !a.remplace_par_id && !a.remplace_par_guest_id).map(a => a.membre_id)
|
||||
);
|
||||
const members = allMembers.filter(m => !absentWithoutReplacement.has(m.id));
|
||||
|
||||
const [guests] = await pool.query(
|
||||
`SELECT i.id, i.prenom, i.nom, i.societe, i.activite, i.photo_url,
|
||||
i.telephone, i.site_web, i.email, i.statut,
|
||||
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.date_club = ? AND (i.statut != 'refusee' OR i.statut IS NULL)
|
||||
ORDER BY i.prenom`,
|
||||
[date]
|
||||
);
|
||||
|
||||
const [currentHonorRow] = await pool.query(
|
||||
`SELECT CONCAT(u.prenom, ' ', u.nom) AS currentHonor
|
||||
FROM mises_en_honneur mh
|
||||
JOIN users u ON u.id = mh.membre_id
|
||||
WHERE mh.date = ?
|
||||
LIMIT 1`,
|
||||
[date]
|
||||
);
|
||||
const currentHonor = currentHonorRow[0]?.currentHonor || null;
|
||||
|
||||
const [nextHonorRow] = await pool.query(
|
||||
`SELECT CONCAT(u.prenom, ' ', u.nom) AS nextHonor
|
||||
FROM mises_en_honneur mh
|
||||
JOIN users u ON u.id = mh.membre_id
|
||||
WHERE mh.date > ?
|
||||
ORDER BY mh.date ASC LIMIT 1`,
|
||||
[date]
|
||||
);
|
||||
const nextHonor = nextHonorRow[0]?.nextHonor || null;
|
||||
|
||||
const [events] = await pool.query(
|
||||
`SELECT nom_evenement AS titre, date AS date_event, horaire AS heure_debut, lieu
|
||||
FROM evenements WHERE date >= ? ORDER BY date ASC LIMIT 5`,
|
||||
[date]
|
||||
);
|
||||
|
||||
const [y, m] = date.split('-')
|
||||
const monthStart = `${y}-${m}-01`
|
||||
const lastDay = new Date(y, m, 0).getDate()
|
||||
const monthEnd = `${y}-${m}-${lastDay}`
|
||||
const [recommendations] = await pool.query(
|
||||
`SELECT r.id, r.type, r.nom_prospect, r.ca_realise, r.created_at,
|
||||
u.prenom, u.nom, u.societe
|
||||
FROM recommandations r
|
||||
JOIN users u ON u.id = r.membre_id
|
||||
WHERE r.created_at >= ? AND r.created_at <= ?
|
||||
ORDER BY r.created_at DESC LIMIT 10`,
|
||||
[monthStart, monthEnd]
|
||||
);
|
||||
|
||||
const [yr, mo, dy] = date.split('-')
|
||||
const dateKey = `${dy}${mo}${yr}`
|
||||
const [memberSlides] = await pool.query(
|
||||
'SELECT id, member_id, filename FROM member_slides WHERE date_key = ?',
|
||||
[dateKey]
|
||||
);
|
||||
|
||||
const [topInviters] = await pool.query(
|
||||
`SELECT u.id, u.prenom, u.nom, u.photo_url, COUNT(i.id) AS total
|
||||
FROM users u
|
||||
LEFT JOIN invites i ON i.invite_par_id = u.id AND i.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
WHERE u.actif = 1
|
||||
GROUP BY u.id, u.prenom, u.nom, u.photo_url
|
||||
ORDER BY total DESC LIMIT 3`
|
||||
);
|
||||
|
||||
const [topDonnees] = await pool.query(
|
||||
`SELECT u.id, u.prenom, u.nom, u.photo_url, COUNT(r.id) AS total
|
||||
FROM users u
|
||||
LEFT JOIN recommandations r ON r.membre_id = u.id
|
||||
WHERE u.actif = 1
|
||||
GROUP BY u.id, u.prenom, u.nom, u.photo_url
|
||||
ORDER BY total DESC LIMIT 3`
|
||||
);
|
||||
|
||||
const [topConversion] = await pool.query(
|
||||
`SELECT u.id, u.prenom, u.nom, u.photo_url, COUNT(m2.id) AS total
|
||||
FROM users u
|
||||
LEFT JOIN invites i ON i.invite_par_id = u.id AND i.statut = 'acceptee'
|
||||
LEFT JOIN users m2 ON m2.email = i.email
|
||||
WHERE u.actif = 1
|
||||
GROUP BY u.id, u.prenom, u.nom, u.photo_url
|
||||
HAVING total > 0
|
||||
ORDER BY total DESC LIMIT 3`
|
||||
);
|
||||
|
||||
const [overrides] = await pool.query(
|
||||
'SELECT slide_key, content, hidden, sort_order FROM presentation_overrides WHERE date_key = ?',
|
||||
[dateKey]
|
||||
);
|
||||
|
||||
res.json({ members, guests, absences, currentHonor: currentHonor || null, nextHonor: nextHonor || null, events, recommendations, memberSlides, topInviters, topDonnees, topConversion, overrides });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,55 @@
|
||||
const { Router } = require('express');
|
||||
const pool = require('../config/db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/:date', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
|
||||
try {
|
||||
const { date } = req.params;
|
||||
const [yr, mo, dy] = date.split('-');
|
||||
const dateKey = `${dy}${mo}${yr}`;
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, slide_key, content, hidden, sort_order FROM presentation_overrides WHERE date_key = ? ORDER BY sort_order ASC',
|
||||
[dateKey]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:date', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
|
||||
try {
|
||||
const { date } = req.params;
|
||||
const { overrides } = req.body;
|
||||
const [yr, mo, dy] = date.split('-');
|
||||
const dateKey = `${dy}${mo}${yr}`;
|
||||
if (!Array.isArray(overrides)) return res.status(400).json({ error: 'overrides doit être un tableau' });
|
||||
|
||||
for (const ov of overrides) {
|
||||
const { slide_key, content, hidden, sort_order } = ov;
|
||||
if (!slide_key) continue;
|
||||
await pool.query(
|
||||
`INSERT INTO presentation_overrides (date_key, slide_key, content, hidden, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE content = VALUES(content), hidden = VALUES(hidden), sort_order = VALUES(sort_order)`,
|
||||
[dateKey, slide_key, content ? JSON.stringify(content) : null, hidden ? 1 : 0, sort_order ?? 0]
|
||||
);
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(
|
||||
'SELECT slide_key, content, hidden, sort_order FROM presentation_overrides WHERE date_key = ? ORDER BY sort_order ASC',
|
||||
[dateKey]
|
||||
);
|
||||
res.json({ overrides: rows });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,175 @@
|
||||
const { Router } = require('express');
|
||||
const pool = require('../config/db');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
const webPush = require('web-push');
|
||||
|
||||
const router = Router();
|
||||
|
||||
const publicVapidKey = process.env.VAPID_PUBLIC_KEY;
|
||||
const privateVapidKey = process.env.VAPID_PRIVATE_KEY;
|
||||
|
||||
if (publicVapidKey && privateVapidKey) {
|
||||
webPush.setVapidDetails('mailto:pierre@secourit.fr', publicVapidKey, privateVapidKey);
|
||||
}
|
||||
|
||||
router.post('/subscribe', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { endpoint, keys, reminderDays } = req.body;
|
||||
if (!endpoint || !keys?.p256dh || !keys?.auth) {
|
||||
return res.status(400).json({ error: 'endpoint, p256dh, and auth required' });
|
||||
}
|
||||
await pool.query(
|
||||
`INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth, reminder_days)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE p256dh = VALUES(p256dh), auth = VALUES(auth), reminder_days = VALUES(reminder_days)`,
|
||||
[req.user.id, endpoint, keys.p256dh, keys.auth, reminderDays || 3]
|
||||
);
|
||||
res.json({ message: 'Abonnement aux notifications enregistré' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/unsubscribe', authenticate, async (req, res) => {
|
||||
try {
|
||||
const { endpoint } = req.body;
|
||||
if (!endpoint) return res.status(400).json({ error: 'endpoint required' });
|
||||
await pool.query('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [req.user.id, endpoint]);
|
||||
res.json({ message: 'Désabonné' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/subscriptions', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT id, endpoint, reminder_days, last_sent_at, created_at FROM push_subscriptions WHERE user_id = ?',
|
||||
[req.user.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/vapid-public-key', (req, res) => {
|
||||
res.json({ publicKey: publicVapidKey });
|
||||
});
|
||||
|
||||
router.get('/members-with-push', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.prenom, u.nom, u.societe,
|
||||
(SELECT COUNT(*) FROM push_subscriptions ps WHERE ps.user_id = u.id) AS has_push
|
||||
FROM users u WHERE u.actif = 1 ORDER BY u.prenom`
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/broadcast', authenticate, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin seulement' });
|
||||
try {
|
||||
const { title, body, url, userIds } = req.body;
|
||||
if (!title || !body) return res.status(400).json({ error: 'Titre et message requis' });
|
||||
|
||||
let subs;
|
||||
if (userIds && Array.isArray(userIds) && userIds.length > 0) {
|
||||
const placeholders = userIds.map(() => '?').join(',');
|
||||
[subs] = await pool.query(
|
||||
`SELECT endpoint, p256dh, auth, user_id FROM push_subscriptions WHERE user_id IN (${placeholders})`,
|
||||
userIds
|
||||
);
|
||||
} else {
|
||||
[subs] = await pool.query('SELECT endpoint, p256dh, auth, user_id FROM push_subscriptions');
|
||||
}
|
||||
|
||||
if (subs.length === 0) return res.status(400).json({ error: 'Aucun abonné' });
|
||||
|
||||
const [notifResult] = await pool.query(
|
||||
'INSERT INTO notifications (sender_id, title, body) VALUES (?, ?, ?)',
|
||||
[req.user.id, title.slice(0, 100), body.slice(0, 255)]
|
||||
);
|
||||
const notifId = notifResult.insertId;
|
||||
|
||||
const recipientIds = [...new Set(subs.map((s) => s.user_id))];
|
||||
for (const uid of recipientIds) {
|
||||
await pool.query(
|
||||
'INSERT IGNORE INTO notification_recipients (notification_id, user_id) VALUES (?, ?)',
|
||||
[notifId, uid]
|
||||
);
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
title: title.slice(0, 100),
|
||||
body: body.slice(0, 255),
|
||||
icon: '/logo.svg',
|
||||
badge: '/favicon.svg',
|
||||
data: { url: '/notifications' },
|
||||
});
|
||||
|
||||
let sent = 0;
|
||||
for (const sub of subs) {
|
||||
try {
|
||||
await webPush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
payload
|
||||
);
|
||||
sent++;
|
||||
} catch (err) {
|
||||
if (err.statusCode === 410 || err.statusCode === 404) {
|
||||
await pool.query('DELETE FROM push_subscriptions WHERE endpoint = ?', [sub.endpoint]);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json({ message: `Notification envoyée à ${sent} abonné(s) sur ${recipientIds.length} destinataire(s)` });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/test', authenticate, async (req, res) => {
|
||||
try {
|
||||
const [subs] = await pool.query(
|
||||
'SELECT endpoint, p256dh, auth FROM push_subscriptions WHERE user_id = ?',
|
||||
[req.user.id]
|
||||
);
|
||||
if (subs.length === 0) return res.status(400).json({ error: 'Aucun abonnement trouvé' });
|
||||
|
||||
const payload = JSON.stringify({
|
||||
title: 'Dyna Biz Coignières',
|
||||
body: 'Notification de test ✓',
|
||||
icon: '/logo.svg',
|
||||
badge: '/favicon.svg',
|
||||
data: { url: '/' },
|
||||
});
|
||||
|
||||
for (const sub of subs) {
|
||||
try {
|
||||
await webPush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
payload
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.statusCode === 410 || err.statusCode === 404) {
|
||||
await pool.query('DELETE FROM push_subscriptions WHERE endpoint = ?', [sub.endpoint]);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json({ message: 'Notification de test envoyée' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,13 @@
|
||||
const { Router } = require('express');
|
||||
const { list, getById, create, update, remove, setStatus } = require('../controllers/recommendationController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/', authenticate, list);
|
||||
router.get('/:id', authenticate, getById);
|
||||
router.post('/', authenticate, create);
|
||||
router.put('/:id', authenticate, update);
|
||||
router.patch('/:id/status', authenticate, setStatus);
|
||||
router.delete('/:id', authenticate, remove);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,9 @@
|
||||
const { Router } = require('express');
|
||||
const { dashboard, memberStats } = require('../controllers/statsController');
|
||||
const { authenticate } = require('../middleware/auth');
|
||||
|
||||
const router = Router();
|
||||
router.get('/dashboard', authenticate, dashboard);
|
||||
router.get('/member/:id?', authenticate, memberStats);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,39 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
const members = [
|
||||
{ email: 'pierre@dynabiz.fr', password: 'Pierre78@' },
|
||||
{ email: 'estelle@dynabiz.fr', password: 'Estelle78@' },
|
||||
{ email: 'patricia@dynabiz.fr', password: 'Patricia78@' },
|
||||
{ email: 'jennifer@dynabiz.fr', password: 'Jennifer78@' },
|
||||
{ email: 'amandine@dynabiz.fr', password: 'Amandine78@' },
|
||||
{ email: 'anais@dynabiz.fr', password: 'Anais78@' },
|
||||
{ email: 'bastien@dynabiz.fr', password: 'Bastien78@' },
|
||||
{ email: 'henrique@dynabiz.fr', password: 'Henrique78@' },
|
||||
{ email: 'laetitia@dynabiz.fr', password: 'Laetitia78@' },
|
||||
{ email: 'marie@dynabiz.fr', password: 'Marie78@' },
|
||||
{ email: 'miguel@dynabiz.fr', password: 'Miguel78@' },
|
||||
{ email: 'seb@dynabiz.fr', password: 'Seb78@' },
|
||||
{ email: 'christelle@dynabiz.fr', password: 'Christelle78@' },
|
||||
{ email: 'mathieu@dynabiz.fr', password: 'Mathieu78@' },
|
||||
{ email: 'morgane@dynabiz.fr', password: 'Morgane78@' },
|
||||
];
|
||||
|
||||
async function seed() {
|
||||
const connection = await mysql.createConnection({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT, 10) || 3306,
|
||||
user: process.env.DB_USER || 'dynausr',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'dynasqy',
|
||||
});
|
||||
for (const m of members) {
|
||||
const hash = await bcrypt.hash(m.password, 10);
|
||||
await connection.execute('UPDATE users SET password_hash = ? WHERE email = ?', [hash, m.email]);
|
||||
}
|
||||
console.log('Mots de passe individuels crees !');
|
||||
for (const m of members) console.log(' ' + m.email.split('@')[0] + ' -> ' + m.password);
|
||||
await connection.end();
|
||||
}
|
||||
seed().catch(console.error);
|
||||
@@ -0,0 +1,68 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
require('dotenv').config();
|
||||
|
||||
const db = require('./config/db');
|
||||
|
||||
const authRoutes = require('./routes/auth');
|
||||
const memberRoutes = require('./routes/members');
|
||||
const guestRoutes = require('./routes/guests');
|
||||
const recommendationRoutes = require('./routes/recommendations');
|
||||
const honorRoutes = require('./routes/honors');
|
||||
const eventRoutes = require('./routes/events');
|
||||
const statsRoutes = require('./routes/stats');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
const invitationRoutes = require('./routes/invitation');
|
||||
const absenceRoutes = require('./routes/absences');
|
||||
const galleryRoutes = require('./routes/gallery');
|
||||
const pushRoutes = require('./routes/push');
|
||||
const notificationRoutes = require('./routes/notifications');
|
||||
const presentationRoutes = require('./routes/presentation');
|
||||
const memberSlidesRoutes = require('./routes/memberSlides');
|
||||
const exportRoutes = require('./routes/export');
|
||||
const activityLogRoutes = require('./routes/activityLog');
|
||||
const presentationEditorRoutes = require('./routes/presentationEditor');
|
||||
const { startPushCron } = require('./services/pushNotifications');
|
||||
|
||||
const app = express();
|
||||
|
||||
const uploadsDir = path.join(__dirname, 'uploads');
|
||||
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
|
||||
const galleryDir = path.join(__dirname, 'gallery');
|
||||
if (!fs.existsSync(galleryDir)) fs.mkdirSync(galleryDir, { recursive: true });
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
app.use('/uploads', express.static(uploadsDir));
|
||||
app.use('/gallery', express.static(galleryDir));
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/members', memberRoutes);
|
||||
app.use('/api/guests', guestRoutes);
|
||||
app.use('/api/recommendations', recommendationRoutes);
|
||||
app.use('/api/honors', honorRoutes);
|
||||
app.use('/api/events', eventRoutes);
|
||||
app.use('/api/stats', statsRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/admin/presentation-editor', presentationEditorRoutes);
|
||||
app.use('/api/public/invitation', invitationRoutes);
|
||||
app.use('/invitation', invitationRoutes);
|
||||
app.use('/api/absences', absenceRoutes);
|
||||
app.use('/api/gallery', galleryRoutes);
|
||||
app.use('/api/push', pushRoutes);
|
||||
app.use('/api/notifications', notificationRoutes);
|
||||
app.use('/api/presentation', presentationRoutes);
|
||||
app.use('/api/member-slides', memberSlidesRoutes);
|
||||
app.use('/api/export', exportRoutes);
|
||||
app.use('/api/activity-logs', activityLogRoutes);
|
||||
app.use('/member-slides', express.static(path.join(__dirname, 'member_slides')));
|
||||
|
||||
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
|
||||
|
||||
startPushCron();
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => console.log(`Backend running on port ${PORT}`));
|
||||
@@ -0,0 +1,28 @@
|
||||
const pool = require('../config/db');
|
||||
|
||||
async function logActivity(reqOrUser, action, details = null) {
|
||||
let userId, userPrenom, userNom, ip;
|
||||
if (reqOrUser && reqOrUser.user) {
|
||||
userId = reqOrUser.user.id;
|
||||
userPrenom = reqOrUser.user.prenom;
|
||||
userNom = reqOrUser.user.nom;
|
||||
ip = reqOrUser.ip || reqOrUser.connection?.remoteAddress || null;
|
||||
} else if (reqOrUser && reqOrUser.id) {
|
||||
userId = reqOrUser.id;
|
||||
userPrenom = reqOrUser.prenom || '';
|
||||
userNom = reqOrUser.nom || '';
|
||||
ip = null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await pool.query(
|
||||
'INSERT INTO activity_logs (user_id, user_prenom, user_nom, action, details, ip_address) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[userId, userPrenom, userNom, action, details ? JSON.stringify(details) : null, ip]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to log activity:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { logActivity };
|
||||
@@ -0,0 +1,86 @@
|
||||
const cron = require('node-cron');
|
||||
const webPush = require('web-push');
|
||||
const pool = require('../config/db');
|
||||
|
||||
const publicVapidKey = process.env.VAPID_PUBLIC_KEY;
|
||||
const privateVapidKey = process.env.VAPID_PRIVATE_KEY;
|
||||
|
||||
if (publicVapidKey && privateVapidKey) {
|
||||
webPush.setVapidDetails('mailto:pierre@secourit.fr', publicVapidKey, privateVapidKey);
|
||||
}
|
||||
|
||||
const REMINDER_MESSAGES = [
|
||||
{
|
||||
title: 'Dyna Biz Coignières',
|
||||
body: 'Pensez à mettre à jour vos invités et recommandations !',
|
||||
icon: '/logo.svg',
|
||||
badge: '/favicon.svg',
|
||||
},
|
||||
];
|
||||
|
||||
function getNextReminderDate(days) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + days);
|
||||
d.setHours(9, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
async function sendReminders() {
|
||||
console.log('[Push Cron] Checking reminders...');
|
||||
try {
|
||||
const [subscriptions] = await pool.query(
|
||||
`SELECT ps.*, u.prenom, u.nom
|
||||
FROM push_subscriptions ps
|
||||
JOIN users u ON u.id = ps.user_id
|
||||
WHERE ps.last_sent_at IS NULL
|
||||
OR DATE_ADD(ps.last_sent_at, INTERVAL ps.reminder_days DAY) <= NOW()`
|
||||
);
|
||||
|
||||
console.log(`[Push Cron] Found ${subscriptions.length} subscription(s) to notify`);
|
||||
|
||||
for (const sub of subscriptions) {
|
||||
try {
|
||||
const msg = REMINDER_MESSAGES[0];
|
||||
const payload = JSON.stringify({
|
||||
title: msg.title,
|
||||
body: msg.body,
|
||||
icon: msg.icon,
|
||||
badge: msg.badge,
|
||||
data: { url: '/' },
|
||||
});
|
||||
|
||||
await webPush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
payload
|
||||
);
|
||||
|
||||
await pool.query('UPDATE push_subscriptions SET last_sent_at = NOW() WHERE id = ?', [sub.id]);
|
||||
console.log(`[Push Cron] Sent to ${sub.prenom} ${sub.nom}`);
|
||||
} catch (err) {
|
||||
if (err.statusCode === 410 || err.statusCode === 404) {
|
||||
await pool.query('DELETE FROM push_subscriptions WHERE id = ?', [sub.id]);
|
||||
console.log(`[Push Cron] Removed expired subscription for ${sub.prenom} ${sub.nom}`);
|
||||
} else {
|
||||
console.error(`[Push Cron] Error sending to ${sub.prenom} ${sub.nom}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Push Cron] Error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function startPushCron() {
|
||||
if (!publicVapidKey || !privateVapidKey) {
|
||||
console.log('[Push Cron] VAPID keys not configured — skipping cron start');
|
||||
return;
|
||||
}
|
||||
|
||||
cron.schedule('0 9 * * *', () => {
|
||||
sendReminders();
|
||||
});
|
||||
|
||||
console.log('[Push Cron] Scheduled daily at 09:00');
|
||||
}
|
||||
|
||||
module.exports = { startPushCron, sendReminders };
|
||||