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