Initial commit: Club DynaSQY
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
.env
|
||||
uploads/
|
||||
gallery/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,97 @@
|
||||
# Club DynaSQY - Application de Gestion
|
||||
|
||||
Application web de gestion du club **Dyna Biz Coignières** : membres, invités, événements, honneurs/recommandations, absences, galerie photos, invitations, notifications push, éditeur de présentations/slideshow, exports et logs d'activité.
|
||||
|
||||
## Stack technique
|
||||
|
||||
| Composant | Technologie |
|
||||
|-----------|------------|
|
||||
| Backend | Node.js 20 + Express |
|
||||
| Frontend | React 18 + Vite 5 + Tailwind 4 |
|
||||
| Base de données | MariaDB 10.11 (Docker) |
|
||||
| Authentification | JWT (jsonwebtoken) |
|
||||
| Notifications push | web-push (VAPID) |
|
||||
| Emails | Nodemailer (SMTP Infomaniak) |
|
||||
| PWA | vite-plugin-pwa (service worker injectManifest) |
|
||||
|
||||
## Déploiement
|
||||
|
||||
```bash
|
||||
cd /opt/dyna
|
||||
docker compose up -d
|
||||
|
||||
# Initialiser les mots de passe des utilisateurs
|
||||
docker compose exec backend node seed.js
|
||||
```
|
||||
|
||||
L'application est accessible sur le port **3001** (frontend nginx) qui proxy vers le backend sur le port **3000**.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
/opt/dyna/
|
||||
├── backend/
|
||||
│ ├── server.js # Point d'entrée Express
|
||||
│ ├── config/db.js # Connexion MariaDB
|
||||
│ ├── middleware/auth.js # JWT middleware
|
||||
│ ├── controllers/ # Logique métier
|
||||
│ ├── routes/ # Routes API
|
||||
│ └── seed.js # Initialisation des comptes
|
||||
├── frontend/
|
||||
│ └── src/ # Composants React + PWA
|
||||
├── database/ # Scripts SQL
|
||||
├── uploads/ # Fichiers uploadés (bind mount)
|
||||
├── gallery/ # Photos galerie (bind mount)
|
||||
├── docker-compose.yml # Stack complète
|
||||
└── nginx.conf # Config reverse proxy
|
||||
```
|
||||
|
||||
## Variables d'environnement (backend)
|
||||
|
||||
| Variable | Description | Valeur par défaut |
|
||||
|----------|-------------|-------------------|
|
||||
| `DB_HOST` | Host MariaDB | `db` (service Docker) |
|
||||
| `DB_PORT` | Port MariaDB | `3306` |
|
||||
| `DB_USER` | Utilisateur BDD | `dynausr` |
|
||||
| `DB_PASSWORD` | Mot de passe BDD | `L0relei69` |
|
||||
| `DB_NAME` | Nom de la BDD | `dynasqy` |
|
||||
| `JWT_SECRET` | Secret JWT | `L0relei69SuperSecret2026` |
|
||||
| `PORT` | Port backend | `3000` |
|
||||
| `SMTP_HOST` | Serveur SMTP | `mail.infomaniak.com` |
|
||||
| `SMTP_PORT` | Port SMTP | `465` |
|
||||
| `SMTP_USER` | Email SMTP | `dyna@secourit.fr` |
|
||||
| `SMTP_PASS` | Mot de passe SMTP | `cAq0cF9t41BE458sdAuzea45215` |
|
||||
| `EMAIL_FROM` | Email expéditeur | `dyna@secourit.fr` |
|
||||
| `PUBLIC_URL` | URL publique | `https://dyna.secourit.fr` |
|
||||
| `VAPID_PUBLIC_KEY` | Clé publique VAPID | configuré dans docker-compose |
|
||||
| `VAPID_PRIVATE_KEY` | Clé privée VAPID | configuré dans docker-compose |
|
||||
|
||||
## Développement
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
npm install
|
||||
node seed.js # Générer les mots de passe
|
||||
node server.js # Démarre sur :3000
|
||||
|
||||
# Frontend
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # Vite sur :5173 avec proxy /api -> :3000
|
||||
```
|
||||
|
||||
## Domaine de production
|
||||
|
||||
- **URL** : https://dyna.secourit.fr
|
||||
- **LXC** : 103 (IP 10.10.10.20)
|
||||
- **SSL** : Let's Encrypt via Nginx Proxy Manager
|
||||
- **Preprod** : https://dyna-preprod.secourit.fr (LXC 110, port 3101)
|
||||
|
||||
## Comptes de test
|
||||
|
||||
| Rôle | Email | Mot de passe |
|
||||
|------|-------|--------------|
|
||||
| Admin | admin@dynasqy.fr | admin123 |
|
||||
| Membre | jean.dupont@email.fr | member123 |
|
||||
| Membre | marie.durand@email.fr | member123 |
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users MODIFY COLUMN reception_connu INT UNSIGNED DEFAULT NULL;
|
||||
ALTER TABLE users MODIFY COLUMN reception_inconnu INT UNSIGNED DEFAULT NULL;
|
||||
@@ -0,0 +1,4 @@
|
||||
UPDATE users SET reception_connu = NULL WHERE reception_connu = 0 OR reception_connu NOT IN (SELECT id FROM users);
|
||||
UPDATE users SET reception_inconnu = NULL WHERE reception_inconnu = 0 OR reception_inconnu NOT IN (SELECT id FROM users);
|
||||
ALTER TABLE users ADD FOREIGN KEY (reception_connu) REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE users ADD FOREIGN KEY (reception_inconnu) REFERENCES users(id) ON DELETE SET NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
SHOW COLUMNS FROM invites like 'visit_number';
|
||||
ALTER TABLE invites ADD COLUMN visit_number INT UNSIGNED NOT NULL DEFAULT 1 AFTER photo_url;
|
||||
SHOW COLUMNS FROM invites like 'visit_number';
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS absences (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
membre_id INT UNSIGNED NOT NULL,
|
||||
date_absence DATE NOT NULL,
|
||||
motif VARCHAR(255) DEFAULT NULL,
|
||||
created_by INT UNSIGNED NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (membre_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
) ENGINE=InnoDB;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE absences
|
||||
ADD COLUMN prevenu TINYINT(1) NOT NULL DEFAULT 0 AFTER motif,
|
||||
ADD COLUMN remplace_par_id INT UNSIGNED DEFAULT NULL AFTER prevenu,
|
||||
ADD FOREIGN KEY (remplace_par_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE absences
|
||||
ADD COLUMN remplace_par_type ENUM('member','guest') DEFAULT 'member' AFTER remplace_par_id,
|
||||
ADD COLUMN remplace_par_guest_id INT UNSIGNED DEFAULT NULL AFTER remplace_par_type,
|
||||
ADD FOREIGN KEY (remplace_par_guest_id) REFERENCES invites(id) ON DELETE SET NULL;
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE IF NOT EXISTS activity_logs (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
user_prenom VARCHAR(100) NOT NULL,
|
||||
user_nom VARCHAR(100) NOT NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
details TEXT DEFAULT NULL,
|
||||
ip_address VARCHAR(45) DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_action (action)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE invites ADD COLUMN statut_candidature VARCHAR(20) DEFAULT NULL;
|
||||
ALTER TABLE invites ADD COLUMN candidature_date TIMESTAMP NULL DEFAULT NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD COLUMN fonction VARCHAR(255) DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN fonction_emoji VARCHAR(10) DEFAULT NULL;
|
||||
ALTER TABLE users ADD COLUMN kdrive_link TEXT DEFAULT NULL;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Migration: add email invitation columns to invites table
|
||||
-- Run this on the existing database before deploying the new code
|
||||
|
||||
ALTER TABLE invites
|
||||
ADD COLUMN statut ENUM('en_attente','acceptee','refusee') NOT NULL DEFAULT 'en_attente'
|
||||
AFTER mail_envoye,
|
||||
ADD COLUMN token VARCHAR(64) DEFAULT NULL
|
||||
AFTER statut,
|
||||
ADD COLUMN invitation_sent_at TIMESTAMP NULL DEFAULT NULL
|
||||
AFTER token,
|
||||
ADD COLUMN photo_url VARCHAR(255) DEFAULT NULL
|
||||
AFTER invitation_sent_at,
|
||||
ADD COLUMN site_web VARCHAR(255) DEFAULT NULL
|
||||
AFTER photo_url,
|
||||
ADD COLUMN visit_number INT UNSIGNED NOT NULL DEFAULT 1
|
||||
AFTER photo_url;
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN invite_par_id INT UNSIGNED DEFAULT NULL AFTER site_web,
|
||||
ADD COLUMN date_postulation DATE DEFAULT NULL AFTER invite_par_id,
|
||||
ADD COLUMN reception_connu TINYINT(1) NOT NULL DEFAULT 0 AFTER date_postulation,
|
||||
ADD COLUMN reception_inconnu TINYINT(1) NOT NULL DEFAULT 0 AFTER reception_connu,
|
||||
ADD COLUMN date_entretien_postulation DATE DEFAULT NULL AFTER reception_inconnu,
|
||||
ADD COLUMN date_demande_whatsapp DATE DEFAULT NULL AFTER date_entretien_postulation,
|
||||
ADD FOREIGN KEY (invite_par_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN merged_display_name VARCHAR(255) DEFAULT NULL AFTER photo_url;
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
sender_id INT UNSIGNED NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_recipients (
|
||||
notification_id INT UNSIGNED NOT NULL,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
read_at DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (notification_id, user_id),
|
||||
FOREIGN KEY (notification_id) REFERENCES notifications(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS presentation_overrides (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
date_key VARCHAR(8) NOT NULL,
|
||||
slide_key VARCHAR(100) NOT NULL,
|
||||
content JSON DEFAULT NULL,
|
||||
hidden TINYINT(1) NOT NULL DEFAULT 0,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_date_slide (date_key, slide_key)
|
||||
) ENGINE=InnoDB;
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
p256dh VARCHAR(255) NOT NULL,
|
||||
auth VARCHAR(255) NOT NULL,
|
||||
reminder_days INT UNSIGNED NOT NULL DEFAULT 3,
|
||||
last_sent_at DATETIME DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_user_sub (user_id, endpoint(255))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE users
|
||||
MODIFY COLUMN reception_connu INT UNSIGNED DEFAULT NULL,
|
||||
MODIFY COLUMN reception_inconnu INT UNSIGNED DEFAULT NULL,
|
||||
ADD FOREIGN KEY (reception_connu) REFERENCES users(id) ON DELETE SET NULL,
|
||||
ADD FOREIGN KEY (reception_inconnu) REFERENCES users(id) ON DELETE SET NULL;
|
||||
@@ -0,0 +1,168 @@
|
||||
CREATE DATABASE IF NOT EXISTS dynasqy
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE dynasqy;
|
||||
|
||||
CREATE TABLE users (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
prenom VARCHAR(100) NOT NULL,
|
||||
nom VARCHAR(100) NOT NULL,
|
||||
societe VARCHAR(255) DEFAULT NULL,
|
||||
activite VARCHAR(255) DEFAULT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
telephone VARCHAR(30) DEFAULT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','member') NOT NULL DEFAULT 'member',
|
||||
date_adhesion DATE DEFAULT NULL,
|
||||
date_echeance DATE DEFAULT NULL,
|
||||
actif TINYINT(1) NOT NULL DEFAULT 1,
|
||||
invite_par_id INT UNSIGNED DEFAULT NULL,
|
||||
date_postulation DATE DEFAULT NULL,
|
||||
reception_connu INT UNSIGNED DEFAULT NULL,
|
||||
reception_inconnu INT UNSIGNED DEFAULT NULL,
|
||||
date_entretien_postulation DATE DEFAULT NULL,
|
||||
date_demande_whatsapp DATE DEFAULT NULL,
|
||||
fonction VARCHAR(255) DEFAULT NULL,
|
||||
fonction_emoji VARCHAR(10) DEFAULT NULL,
|
||||
kdrive_link TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (invite_par_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (reception_connu) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (reception_inconnu) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE invites (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
prenom VARCHAR(100) NOT NULL,
|
||||
nom VARCHAR(100) NOT NULL,
|
||||
societe VARCHAR(255) DEFAULT NULL,
|
||||
activite VARCHAR(255) DEFAULT NULL,
|
||||
telephone VARCHAR(30) DEFAULT NULL,
|
||||
email VARCHAR(255) DEFAULT NULL,
|
||||
site_web VARCHAR(255) DEFAULT NULL,
|
||||
commentaires TEXT DEFAULT NULL,
|
||||
mail_envoye TINYINT(1) NOT NULL DEFAULT 0,
|
||||
statut ENUM('en_attente','acceptee','refusee') NOT NULL DEFAULT 'en_attente',
|
||||
token VARCHAR(64) DEFAULT NULL,
|
||||
invitation_sent_at TIMESTAMP NULL DEFAULT NULL,
|
||||
photo_url VARCHAR(255) DEFAULT NULL,
|
||||
visit_number INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
date_club DATE DEFAULT NULL,
|
||||
statut_candidature VARCHAR(20) DEFAULT NULL,
|
||||
candidature_date TIMESTAMP NULL DEFAULT NULL,
|
||||
invite_par_id INT UNSIGNED NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (invite_par_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE recommandations (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
membre_id INT UNSIGNED NOT NULL,
|
||||
destinataire_id INT UNSIGNED DEFAULT NULL,
|
||||
nom_prospect VARCHAR(255) NOT NULL,
|
||||
type ENUM('donnee','recue') NOT NULL DEFAULT 'donnee',
|
||||
transformee TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ca_realise DECIMAL(12,2) DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (membre_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (destinataire_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE mises_en_honneur (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
membre_id INT UNSIGNED DEFAULT NULL,
|
||||
societe VARCHAR(255) DEFAULT NULL,
|
||||
theme_sujet VARCHAR(500) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (membre_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE evenements (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
nom_evenement VARCHAR(255) NOT NULL,
|
||||
lieu VARCHAR(255) DEFAULT NULL,
|
||||
horaire VARCHAR(50) DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS absences (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
membre_id INT UNSIGNED NOT NULL,
|
||||
date_absence DATE NOT NULL,
|
||||
motif VARCHAR(255) DEFAULT NULL,
|
||||
prevenu TINYINT(1) NOT NULL DEFAULT 0,
|
||||
remplace_par_id INT UNSIGNED DEFAULT NULL,
|
||||
remplace_par_type ENUM('member','guest') DEFAULT 'member',
|
||||
remplace_par_guest_id INT UNSIGNED DEFAULT NULL,
|
||||
created_by INT UNSIGNED NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (membre_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (remplace_par_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (remplace_par_guest_id) REFERENCES invites(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE presences (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
membre_id INT UNSIGNED NOT NULL,
|
||||
date_reunion DATE NOT NULL,
|
||||
present TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (membre_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_membre_reunion (membre_id, date_reunion)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE member_slides (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
member_id INT UNSIGNED NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
date_key VARCHAR(8) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (member_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO users (prenom, nom, societe, activite, email, telephone, password_hash, role, date_adhesion, date_echeance) VALUES
|
||||
('Pierre', 'Admin', 'Dyna Biz', 'Administration', 'pierre@dynabiz.fr', '0600000001', '$2b$10$seedplaceholder','admin', '2024-01-01','2026-12-31'),
|
||||
('Estelle', 'Admin', 'Dyna Biz', 'Administration', 'estelle@dynabiz.fr', '0600000002', '$2b$10$seedplaceholder','admin', '2024-01-01','2026-12-31'),
|
||||
('Patricia', 'Membre', '', '', 'patricia@dynabiz.fr', '0600000003', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Jennifer', 'Membre', '', '', 'jennifer@dynabiz.fr', '0600000004', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Amandine', 'Membre', '', '', 'amandine@dynabiz.fr', '0600000005', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Anais', 'Membre', '', '', 'anais@dynabiz.fr', '0600000006', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Bastien', 'Membre', '', '', 'bastien@dynabiz.fr', '0600000007', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Henrique', 'Membre', '', '', 'henrique@dynabiz.fr', '0600000008', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Laetitia', 'Membre', '', '', 'laetitia@dynabiz.fr', '0600000009', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Marie', 'Membre', '', '', 'marie@dynabiz.fr', '0600000010', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Miguel', 'Membre', '', '', 'miguel@dynabiz.fr', '0600000011', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Seb', 'Membre', '', '', 'seb@dynabiz.fr', '0600000012', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Christelle','Membre', '', '', 'christelle@dynabiz.fr', '0600000013', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Mathieu', 'Membre', '', '', 'mathieu@dynabiz.fr', '0600000014', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31'),
|
||||
('Morgane', 'Membre', '', '', 'morgane@dynabiz.fr', '0600000015', '$2b$10$seedplaceholder','member', '2024-01-01','2026-12-31');
|
||||
|
||||
INSERT INTO evenements (date, nom_evenement, lieu, horaire) VALUES
|
||||
('2025-03-14','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2025-05-09','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2025-06-13','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2025-09-12','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2025-11-14','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2025-12-12','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-01-13','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-01-27','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-02-10','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-02-24','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-03-10','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-03-24','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-04-07','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-04-21','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-05-05','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-06-02','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-06-12','Rendez-vous Club d''affaires','Hotel IBIS SQY','08h30'),
|
||||
('2026-06-16','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-07-10','After-work Club','Le Bistrot SQY','18h30'),
|
||||
('2026-07-28','Petit-déjeuner réseau','Hotel IBIS SQY','08h30'),
|
||||
('2026-09-11','Réunion plénière','CCI SQY','09h00');
|
||||
@@ -0,0 +1,201 @@
|
||||
DELETE FROM invites;
|
||||
-- Import from Dyna Biz CSV (UTF-8 encoded)
|
||||
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Guillaume', 'BALDECK', 'Mercure', 'Hôtellerie Restauration', '06 50 00 87 52', 'Guillaume.BALDECK@accor.com', '2026-01-13', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Morgane', 'MONTEL', 'Generali', 'Assurance pro', '06 63 70 28 55', 'morgane.montel@generali.com', '2026-01-13', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Morgane', 'MONTEL', 'Generali', 'Assurance pro', '06 63 70 28 55', 'morgane.montel@generali.com', '2026-01-27', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Lou', 'MENANT', 'Ninphi', 'Conseil aux organisations, transition, management, qualité, QVCT', '06 11 17 53 08', 'lmenant@ninphi.fr', '2026-01-27', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Damien', 'RICCIO', 'DARE Consulting', 'Commerce et import-export', '07 87 81 60 71', 'driccio@dare-consulting.eu', '2026-01-27', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Damien', 'RICCIO', 'DARE Consulting', 'Commerce et import-export', '07 87 81 60 71', 'driccio@dare-consulting.eu', '2026-02-24', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Amandine', 'CASSEL', 'Banque populaire', 'Banque', '06 74 26 00 64', 'amandine.cassel@bpvf.fr', '2026-02-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Amandine', 'CASSEL', 'Banque populaire', 'Banque', '06 74 26 00 64', 'amandine.cassel@bpvf.fr', '2026-02-24', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Marc-André', 'CHALMANDRIER', 'SIA', 'Sécurité incendie', '06 87 72 29 44', 'mchalmandrier@sia-service.fr', '2026-02-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Patrick', 'DIAMANT-BERGER', 'QUALRISK', 'Organisme de formation', '06 74 91 54 97', 'patrick.diamant-berger@qualrisk.fr', '2026-02-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Yann', 'RAOUL', 'Phare away', 'conseil communication - évènementiel - sponsoring - brand content - relations publiques', '06 12 66 38 47', 'yann.raoul@phareaway.com', '2026-02-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Yann', 'RAOUL', 'Phare away', 'conseil communication - évènementiel - sponsoring - brand content - relations publiques', '06 12 66 38 47', 'yann.raoul@phareaway.com', '2026-02-24', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Tony', 'ROY', 'Banque Populaire', 'banque', '06 10 72 65 93', 'tony.roy@bpvf.fr', '2026-02-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Anaïs', 'WOESTELANDT', 'RAS Intérim', 'Agence Intérim', '06 20 55 11 71', 'Anais.woestelandt@ras-interim.fr', '2026-02-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Gaëlle', 'CARLIER', 'CBIS Architectes', 'Architecte', '06 16 35 36 87', 'g.carlier@cbis-architectes.com', '2026-02-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Amélie', 'DURIZOT', 'RAS Intérim', 'Agence Intérim', '06 67 96 11 52', 'amelie.durizot@ras-interim.fr', '2026-02-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Amélie', 'DURIZOT', 'RAS Intérim', 'Agence Intérim', '06 67 96 11 52', 'amelie.durizot@ras-interim.fr', '2026-03-10', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Marc', 'LANOY', 'Dunamis solutions', 'QHSE et management', '06 80 38 87 76', 'marc.lanoy@dunamissolutions.fr', '2026-02-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Yun-Tyng', 'SUN', 'Sunshine Conseil', 'Gestionnaire de patrimoine', '06 99 76 21 64', 'sunshine.conseil@outlook.fr', '2026-02-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Yun-Tyng', 'SUN', 'Sunshine Conseil', 'Gestionnaire de patrimoine', '06 99 76 21 64', 'sunshine.conseil@outlook.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Alexandre', 'DUBOST', 'Just Be Flex', 'Application Web', '06 14 34 30 12', 'alexandre.dubost@justbeflex.com', '2026-03-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Christelle', 'GEBELIN', 'CGE Conseil Patrimoine', 'Conseiller en gestion de patrimoine', '06 63 72 19 77', 'christelle.gebelin1@gmail.com', '2026-03-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Christelle', 'GEBELIN', 'CGE Conseil Patrimoine', 'Conseiller en gestion de patrimoine', '06 63 72 19 77', 'christelle.gebelin1@gmail.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Clémence', 'MORET', 'Cool Tech', 'Bâtiment', '07 64 14 03 64', 'contact@cooltech-idf.fr', '2026-03-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Richard', 'WAWRZYNIAK', 'FACET', 'Gestion et communication de crise', '06.89.36.86.27.', 'rwawrzyniak.facet@gmail.com', '2026-03-10', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Alexandre', 'BOULINE', 'Marie Blachère', 'Restauration', '06 34 11 98 00', 'cmabouline@gmail.com', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Alexandre', 'BOULINE', 'Marie Blachère', 'Restauration', '06 34 11 98 00', 'cmabouline@gmail.com', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Cindy', 'COATANEA', 'Pretpro.fr', 'Expert en financement professionnel', '06 89 69 49 29', 'cindy.coatanea@pretpro.expert', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Sandra', 'COCHEME', 'Expert Gestion GCL', 'Gestion d''entreprise', '06 88 90 54 62', 's.cocheme@expertgcl.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Nadir', 'HAMMOUCHE', '4pointsur', 'Centre de formation', '07 44 96 43 30', 'nadir.hammouche@4pointsur.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Rémy', 'MAKADJI', 'Perfzone', '', '06 21 23 57 17', 'remy.makadji@p-cub3.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Aurélie', 'STRUZYNSKI', 'AS-UMAN', 'Coach professionnelle et formatrice PCM', '06 19 24 59 82', 'aureliestruzynski@as-uman.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Anne-Sophie', 'VALET', 'Essentionnelle', 'coaching de dirigeants', '06 58 07 53 19', 'essensionnel@gmail.com', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'AMAR Manal', 'BEN', 'LMC Partners', 'Avocat', '06 09 60 32 04', 'pole-social1@lmcpartenaires.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Anthony', 'SOUSTELLE', 'Xefi', 'Informatique', '06 08 64 91 92', 'a.soustelle@xefi.fr', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Anne', 'JOUANNE', 'AJC', 'RAF externalisé', '06 62 21 60 61', 'anne.ajconseil@gmail.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Victoire', 'AUBERTIN', 'Formya', 'Formation', '06 71 37 01 39', 'victoire.aubertin@formya.fr', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'FONTE Julio', 'DA', 'JDF RENOV', 'Plombier', '06 75 50 73 05', 'jdfonte@jdfrenov.fr', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Axelle', 'JAQUET', 'Infor Créances', 'Recouvrement', '06 14 30 16 37', 'axelle.jaquet@infor-creances.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Séverine', 'GIRAUDO', 'Girotec', 'Visserie', '06 72 25 84 56', 'severine@girotec.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Cyndie', 'GOMBAULD', 'Dycine consulting', 'Appels d''offres', '06 24 24 47 72', 'contact@dycineconsulting.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Cyndie', 'GOMBAULD', 'Dycine consulting', 'Appels d''offres', '06 24 24 47 72', 'contact@dycineconsulting.com', '2026-06-02', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Cyril', 'CAUDYSER', 'AMS', 'Aménagement d''utilitaire', '06 83 55 71 10', 'cyril@automulltiservices.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Cyril', 'CAUDYSER', 'AMS', 'Aménagement d''utilitaire', '06 83 55 71 10', 'cyril@automulltiservices.com', '2026-06-02', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Maxime', 'GUILLIET', 'FORD', 'Concessionnaire auto', '07 85 96 49 10', 'maxime.guilliet@autosphere.fr', '2026-06-02', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Maxime', 'GUILLIET', 'FORD', 'Concessionnaire auto', '07 85 96 49 10', 'maxime.guilliet@autosphere.fr', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 2);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Mélanie', 'LOUCHART', 'Emulsion MCL', 'Conseil en stratégie d''entreprise', '06 10 90 32 46', 'mel.louchart@gmail.com', '2026-05-05', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'David', 'HERNANDEZ', 'SAVAC', 'Transport de voyageurs', '06 89 52 12 01', 'davidhperso@gmail.com', '2026-04-21', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Valérie', 'PASCUAL', 'Kintsugi', 'Médiateure', '06 67 11 26 07', 'valerie.pascual@kintsugimediation.fr', '2026-06-02', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'BEAUMONT Maryline', 'CHALANDON', 'JH2C', 'Notaire', '06 60 67 16 38', 'maryline.chalandon-beaumont@jh2c.notaires.fr', '2026-04-21', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Wilfried', 'MOISANT', 'Atout Concept', 'Maîtrise d''oeuvre', '07 63 62 17 96', 'wmoisant@moe-atoutconcept.com', '2026-04-21', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'François', 'JORDA', 'Domaines créatifs', 'Espaces verts', '', '', '2026-04-21', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Claire', 'GERMAIN', 'Clairver', 'Accompagnement d''entreprise', '', '', '2026-04-21', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Olivier', 'LEGRAVEREND', 'DC Pilot', 'Direction commerciale externalisée', '06 25 54 28 78', 'olivier.legraverend@dc-pilot.com', '2026-05-05', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Jérémy', 'MALINOSKY', 'MT Cuisines', 'Cuisiniste', '06 12 86 40 51', 'jmalinosky@mtcuisines.fr', '2026-04-21', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Ludovic', 'DESIRE', 'Massy Carrelage', 'Négoce du bâtiment', '07 48 88 47 51', 'massycarrelage@gmail.com', '2026-07-28', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Francine', 'FOURRE', 'Aurizéo', 'Mobilité inclusive', '06 83 02 88 88', 'francine.fourre@aurizeo.com', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Margaux', 'PERRIN', 'Only Brunch', 'Restauration', '07 82 49 01 25', 'margaux.perrin@onlybrunch.fr', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Marie', 'DESARMENIEN', 'Only Brunch', 'Restauration', '', '', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Mathieu', 'DUBOIS', 'Uni Pare-brise', 'Remplacement vitrerie de véhicule', '07 59 70 62 48', 'mjrmdirection.upb@gmail.com', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Williams', 'LAVALEE', 'Green patrimoine', 'Menuiserie', '06 16 16 52 36', 'williams.lavalee@green-patrimoine.fr', '2026-06-02', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Christophe', 'MEDICI', 'HQR', 'Formation', '06 88 35 32 11', 'Christophe.hqr@gmail.com', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Fabien', 'HEMERY', 'BSOP', 'Matériel informatique', '06 75 44 53 02', 'fabien.hemery@bsop78.fr', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Pascal', 'RYBAK', 'Sitadel', 'Solution GMAO pour services généraux', '06 69 74 70 00', 'pascal.rybak@sitadel.fr', '2026-07-28', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Charles', 'GOBESSI', 'Agent Keller Williams Imobilier', 'Agent immobilier', '06 88 52 24 05', 'charles.gobessi@kwfrance.com', '2026-06-16', (SELECT id FROM users WHERE prenom='Estelle'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Damien', 'Dufour', 'GroupeActive', 'DSI en temps partagé', '06 86 07 17 25', '', '2026-04-07', (SELECT id FROM users WHERE prenom='Pierre'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Stéphanie', 'Mrowczynski', 'Web Me Baby', 'Référencement SEO', '07 56 94 39 45', 'stephanie@webmebaby.com', '2026-06-02', (SELECT id FROM users WHERE prenom='Pierre'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Roxane', 'Robin', 'Roxanerobin', 'Graphiste Webdesigner', '06 23 68 78 15', 'robinroxane78@gmail.com', '2026-06-02', (SELECT id FROM users WHERE prenom='Pierre'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Clara', 'Trouchet', 'CDC Projet', 'Consultante RSE', '06 65 37 77 49', 'clara@cdc-projet.fr', '2026-06-02', (SELECT id FROM users WHERE prenom='Pierre'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Laure', 'Préaubert', 'h2o', 'Conseillère h2o', '06 03 56 76 80', 'laure.preaubert@orange.fr', '2026-06-02', (SELECT id FROM users WHERE prenom='Pierre'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Rémy', 'Makadji', 'ActionCOACH 78', 'Accompagnement entreprises', '06 21 23 57 17', 'remy.makadji@p-cub3.fr', '2026-04-21', (SELECT id FROM users WHERE prenom='Jennifer'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Bertrand', 'CLAIRIS', 'ESSPAIE RH', 'Gestion PAIE', '06 07 02 50 05', 'b.clairis@esspaierh.fr', '2026-05-05', (SELECT id FROM users WHERE prenom='Jennifer'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Magali', 'ROUSSEL', 'QB / SETIN', 'Caincaillerie et consommables', '07 87 03 25 60', 'magalie.roussel@setin.fr', '2026-06-02', (SELECT id FROM users WHERE prenom='Jennifer'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Eric', 'BIBAL', 'V&B', 'commerce de vin et bieres', '06 13 08 06 17', 'maurepas@vandb.fr', '2026-06-02', (SELECT id FROM users WHERE prenom='Jennifer'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Akouvi', 'BLEWOUSSI', 'NIRLYS PARIS GROUP', 'Cosmétiques', '06 44 89 59 66', 'nirlyscosmetik78@yahoo.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Patricia'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Audrey', 'LEMAIRE', 'Drevent Photobooth', 'Photobooth', '06 49 46 29 74', 'contact@drevent.fr', '2026-04-07', (SELECT id FROM users WHERE prenom='Patricia'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Cindy', 'COATANEA', 'Pretpro', 'Experte financement professionnels', '06 89 69 49 29', 'cindy.coatanea@pretpro.expert', '2026-04-07', (SELECT id FROM users WHERE prenom='Patricia'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'BORGNE Cédric', 'LE', 'A.2.L.2 Diagnostics Immobiliers', 'Diagnostics', '06 38 62 70 93', 'a.2.l.2@hotmail.fr', '2026-05-05', (SELECT id FROM users WHERE prenom='Patricia'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Guillaume', 'VALLOT', 'Vallotech', 'Systèmes de sécurité', '06 41 77 78 88', 'guillaume.vallot@vallotech.com', '2026-05-05', (SELECT id FROM users WHERE prenom='Patricia'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Joëlle', 'BIGOTTEAU', 'A2J Courtage', 'Courtière', '06 36 46 95 95', 'joelle@a2jcourtage.fr', '2026-06-16', (SELECT id FROM users WHERE prenom='Patricia'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Nathalie', 'ABREU', 'WiiSmile', 'Comité Entreprise Externe', '', 'n.abreu@wiismile.fr', '2026-06-16', (SELECT id FROM users WHERE prenom='Patricia'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'JACOPE', 'Stéphane', 'Axa', 'Assurance', '662611248', 'stephane.jacope.a2p@axa.fr', '2026-04-07', (SELECT id FROM users WHERE prenom='Morgane'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Marchal', 'Nicola', 'Empruntis', 'Courtiage', '757007166', 'nicolas.marchal@empruntis,com', '2026-04-07', (SELECT id FROM users WHERE prenom='Morgane'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Bruno', 'MASSON', 'Résilience RH', 'RH de crise', '06 75 04 02 90', 'contact@resiliencerh.fr', '2026-04-07', (SELECT id FROM users WHERE prenom='Miguel'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Djibril', 'SAMBA', 'Golf Events et Consulting', 'Cours et organisation d''évenements golf', '06 43 10 60 40', 'djibril@golf-events-consulting.fr', '2026-05-05', (SELECT id FROM users WHERE prenom='Miguel'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Broussard', 'Gontran', 'Serious Team 360', 'Communication', '', '', '2026-05-05', (SELECT id FROM users WHERE prenom='Marie'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'PERETTI Antoine', 'DE', 'Trujas', 'Concession automobile', '634446084', 'antoine.deperetti@trujas.carwest.fr', '2026-04-07', (SELECT id FROM users WHERE prenom='Seb'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'David', 'Willim', 'My Bank Privée', 'Financement/Assurance', '06 37 61 98 06', 'davidw@mybankprivee.com', '2026-03-24', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'David', 'Chaumont', 'Forever', 'Bien être', '06 60 85 42 99', 'dchaumont@yahoo.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Sophie', 'Marchand', 'DSM Conseil & Intégration Audiovisuelle', 'Audiovisuel', '06 87 51 44 65', 'sophie.marchand@dsm-conseil.com', '2026-03-24', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Wassim', 'Sayerh', 'Overto', 'Projections financières', '07 68 50 67 72', 'wassim.sayerh@overto.fr', '2026-03-24', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Clémence', 'Gastine', 'METRONOME Partners', 'Droit du travail', '06 29 55 28 24', 'clemence.gastine@metronome-partners.com', '2026-05-05', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Patrick', 'Lavergne', 'Digital & Vous', 'Solutions Web et Numériques', '06 03 88 56 67', 'patrick.lavergne@digital-et-vous.com', '2026-04-07', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Alex', 'Lasnier', 'Data Méchanic', 'Data Scientist', '06 32 46 36 51', 'alex.lasnier30@gmail.com', '2026-04-21', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Maryline', 'Jean', 'DYVERSITY', 'Formation en efficacité interpersonnelle', '06 08 80 17 21', 'maryline.jean@dyversity.fr', '2026-05-05', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Abouldhadid', 'Ouafa', 'FFC', 'Formation spécialisée First et Last Mile, Coaching, Leadership', '06 95 09 58 21', 'ouafa@firme-formationcoaching.fr', '2026-05-05', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Le Mercier', 'Marlène', 'SELARL THOMAZON AUDRANT BICHE ET ASSOCIES', 'Commisaire de Justice', '01 42 33 56 41', 'etude@tb-huissier.com', '2026-05-05', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, date_club, invite_par_id, visit_number) VALUES (
|
||||
'Martin', 'Vincent', 'Experts Gestion GCL', 'Gestion, Conseil', '07 70 08 52 84', 'v.martin@expertgcl.fr', '2026-05-05', (SELECT id FROM users WHERE prenom='Bastien'), 1);
|
||||
|
||||
-- Total unique guests: 88
|
||||
@@ -0,0 +1,13 @@
|
||||
INSERT INTO evenements (nom_evenement, date) VALUES
|
||||
('Club d''Affaires', '2026-01-13'),
|
||||
('Club d''Affaires', '2026-01-27'),
|
||||
('Club d''Affaires', '2026-02-10'),
|
||||
('Club d''Affaires', '2026-02-24'),
|
||||
('Club d''Affaires', '2026-03-10'),
|
||||
('Club d''Affaires', '2026-03-24'),
|
||||
('Club d''Affaires', '2026-04-07'),
|
||||
('Club d''Affaires', '2026-04-21'),
|
||||
('Club d''Affaires', '2026-05-05'),
|
||||
('Club d''Affaires', '2026-06-02'),
|
||||
('Club d''Affaires', '2026-06-16'),
|
||||
('Club d''Affaires', '2026-07-28');
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Seed guest data (invites)
|
||||
-- Run AFTER schema.sql (or after migration_invites.sql on existing DB)
|
||||
-- Uses subqueries to resolve user IDs by prenom
|
||||
|
||||
-- 2025-03-14 (Petit-dejeuner reseau)
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number) VALUES
|
||||
('Helene','Lasne','Distinxion','Distribution','0600000101','helene@distinxion.fr',NULL,NULL,'2025-03-14',(SELECT id FROM users WHERE prenom='Pierre'),1);
|
||||
|
||||
-- 2025-05-09
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number) VALUES
|
||||
('Chakib','Aida','NP','Conseil','0600000102','chakib@np.fr',NULL,NULL,'2025-05-09',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Alassane','Keita','Sericyne','Sante','0600000103','alassane@sericyne.fr',NULL,NULL,'2025-05-09',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Steeven','Auffret','Digital Ynov','Formation','0600000104','steeven@ynov.fr',NULL,NULL,'2025-05-09',(SELECT id FROM users WHERE prenom='Pierre'),1);
|
||||
|
||||
-- 2025-06-13
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number) VALUES
|
||||
('Aurore','Picard d''Estelan','Coaching Pro','Coaching','0600000105','aurore@coachingpro.fr',NULL,NULL,'2025-06-13',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Virginie','Assouad','O2 Coignieres','Services','0600000106','virginie@o2coignieres.fr',NULL,NULL,'2025-06-13',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Thierry','Thabouret','JT','Conseil','0600000107','thierry@jt.fr',NULL,NULL,'2025-06-13',(SELECT id FROM users WHERE prenom='Pierre'),1);
|
||||
|
||||
-- 2025-09-12 (second visits)
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number) VALUES
|
||||
('Angelique','Lebreton','Captive','Communication','0600000108','angelique@captive.fr',NULL,NULL,'2025-09-12',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Stephane','Lebreton','Captive','Communication','0600000109','stephane@captive.fr',NULL,NULL,'2025-09-12',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Alassane','Keita','Sericyne','Sante','0600000103','alassane@sericyne.fr',NULL,NULL,'2025-09-12',(SELECT id FROM users WHERE prenom='Pierre'),2),
|
||||
('Aurore','Picard d''Estelan','Coaching Pro','Coaching','0600000105','aurore@coachingpro.fr',NULL,NULL,'2025-09-12',(SELECT id FROM users WHERE prenom='Pierre'),2);
|
||||
|
||||
-- 2025-11-14
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number) VALUES
|
||||
('Rene','Gatou','L''immobilier du possible','Immobilier','0600000110','rene@immopossible.fr',NULL,NULL,'2025-11-14',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Cyriaque','Djedji','ABC','Conseil','0600000111','cyriaque@abc.fr',NULL,NULL,'2025-11-14',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Catherine','Lefevre','La Fourche','Alimentation','0600000112','catherine@lafourche.fr',NULL,NULL,'2025-11-14',(SELECT id FROM users WHERE prenom='Pierre'),1);
|
||||
|
||||
-- 2025-12-12 (second visits)
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number) VALUES
|
||||
('Cedrick','Coulibaly','Ekyp Management','Management','0600000113','cedrick@ekyp.fr',NULL,NULL,'2025-12-12',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Thierry','Thabouret','JT','Conseil','0600000107','thierry@jt.fr',NULL,NULL,'2025-12-12',(SELECT id FROM users WHERE prenom='Pierre'),2),
|
||||
('Virginie','Assouad','O2 Coignieres','Services','0600000106','virginie@o2coignieres.fr',NULL,NULL,'2025-12-12',(SELECT id FROM users WHERE prenom='Pierre'),2);
|
||||
|
||||
-- 2026-06-12 (Rendez-vous Club d affaires - future)
|
||||
INSERT INTO invites (prenom, nom, societe, activite, telephone, email, site_web, commentaires, date_club, invite_par_id, visit_number) VALUES
|
||||
('Manon','Balesi','Conseil','Conseil','0600000114','manon@conseil.fr',NULL,NULL,'2026-06-12',(SELECT id FROM users WHERE prenom='Pierre'),1),
|
||||
('Cedrick','Coulibaly','Ekyp Management','Management','0600000113','cedrick@ekyp.fr',NULL,NULL,'2026-06-12',(SELECT id FROM users WHERE prenom='Pierre'),2),
|
||||
('Rene','Gatou','L''immobilier du possible','Immobilier','0600000110','rene@immopossible.fr',NULL,NULL,'2026-06-12',(SELECT id FROM users WHERE prenom='Pierre'),2),
|
||||
('Angelique','Lebreton','Captive','Communication','0600000108','angelique@captive.fr',NULL,NULL,'2026-06-12',(SELECT id FROM users WHERE prenom='Pierre'),2),
|
||||
('Stephane','Lebreton','Captive','Communication','0600000109','stephane@captive.fr',NULL,NULL,'2026-06-12',(SELECT id FROM users WHERE prenom='Pierre'),2),
|
||||
('Cyriaque','Djedji','ABC','Conseil','0600000111','cyriaque@abc.fr',NULL,NULL,'2026-06-12',(SELECT id FROM users WHERE prenom='Pierre'),2),
|
||||
('Catherine','Lefevre','La Fourche','Alimentation','0600000112','catherine@lafourche.fr',NULL,NULL,'2026-06-12',(SELECT id FROM users WHERE prenom='Pierre'),2);
|
||||
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.11
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: L0reei69
|
||||
MYSQL_DATABASE: dynasqy
|
||||
MYSQL_USER: dynausr
|
||||
MYSQL_PASSWORD: L0relei69
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
backend:
|
||||
build: ./backend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DB_HOST: db
|
||||
DB_PORT: 3306
|
||||
DB_USER: dynausr
|
||||
DB_PASSWORD: L0relei69
|
||||
DB_NAME: dynasqy
|
||||
JWT_SECRET: L0relei69SuperSecret2026
|
||||
PORT: 3000
|
||||
SMTP_HOST: mail.infomaniak.com
|
||||
SMTP_PORT: 465
|
||||
SMTP_USER: dyna@secourit.fr
|
||||
SMTP_PASS: cAq0cF9t41BE458sdAuzea45215
|
||||
EMAIL_FROM: dyna@secourit.fr
|
||||
PUBLIC_URL: http://localhost:3001
|
||||
VAPID_PUBLIC_KEY: BA3f4_jkSxFKhldhTYbMR0AaMD1ROPE3PwFAW5bw_fVqmqQs6rdZ8kINs6aNWfyaXzaPbZxPDTD3tQbbFSFxmPA
|
||||
VAPID_PRIVATE_KEY: xE1t4NCOZbGhjUgRs_P92sXvlgKeb3-64Vr7mwyiRTg
|
||||
volumes:
|
||||
- /opt/dyna/uploads/uploads:/app/uploads
|
||||
- /opt/dyna/gallery:/app/gallery
|
||||
ports:
|
||||
- "3000:3000"
|
||||
frontend:
|
||||
build: ./frontend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "3001:80"
|
||||
volumes:
|
||||
db_data:
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx-frontend.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#1e40af" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Dyna Biz Coignières</title>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,66 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /invitation/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_request_buffering off;
|
||||
client_max_body_size 300M;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
|
||||
location /uploads/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
client_max_body_size 10M;
|
||||
}
|
||||
|
||||
location = /gallery { try_files $uri /index.html; }
|
||||
location = /gallery/ { try_files $uri /index.html; }
|
||||
|
||||
location /gallery/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
client_max_body_size 300M;
|
||||
}
|
||||
|
||||
location /member-slides/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_request_buffering off;
|
||||
proxy_buffering off;
|
||||
client_max_body_size 300M;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "club-dynasqy-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"cropperjs": "^1.6.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-pwa": "^0.20.1"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 578 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 578 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" rx="20" fill="#1e40af"/>
|
||||
<text x="50" y="65" font-size="40" font-family="Arial" fill="white" text-anchor="middle" font-weight="bold">DS</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 248 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 176 KiB |