Add account-based event registration and robust email HTML
This commit is contained in:
1 parent
c647c90078
commit
e170d57577
7 files changed
+340
-95
No files matched your search
+33
-22
@@ -59,11 +59,16 @@ export function buildInvitationEmail({ rencontre, baseUrl, texts = {} }) {
|
||||
restantes > 0
|
||||
? `${restantes} place${restantes > 1 ? 's' : ''} restante${restantes > 1 ? 's' : ''}`
|
||||
: 'Complet — liste d’attente',
|
||||
r.participants_par_compte
|
||||
? `${r.participants_par_compte} participant${r.participants_par_compte > 1 ? 's' : ''} maximum par compte membre`
|
||||
: '',
|
||||
].filter(Boolean);
|
||||
|
||||
// Spacing belongs on table cells: Outlook's Word renderer commonly drops
|
||||
// padding and margins applied to div/p elements when HTML is pasted.
|
||||
const para = (text, extra = '') =>
|
||||
text
|
||||
? `<div style="font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:1.7;color:#5A544E;${extra}">${escML(text)}</div>`
|
||||
? `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td style="font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:25px;mso-line-height-rule:exactly;color:#5A544E;${extra}">${escML(text)}</td></tr></table>`
|
||||
: '';
|
||||
|
||||
// "Bulletproof" CTA button. Outlook (Word engine) ignores padding and
|
||||
@@ -79,24 +84,29 @@ export function buildInvitationEmail({ rencontre, baseUrl, texts = {} }) {
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="${esc(url)}" target="_blank" style="display:inline-block;width:300px;line-height:48px;background-color:#C1272D;color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;text-align:center;text-decoration:none;border-radius:3px;-webkit-text-size-adjust:none;mso-hide:all;">${label}</a>
|
||||
<a href="${esc(url)}" target="_blank" style="display:inline-block;width:300px;line-height:48px;background-color:#C1272D;color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;text-align:center;text-decoration:none;border-radius:3px;-webkit-text-size-adjust:none;">${label}</a>
|
||||
<!--<![endif]-->`;
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html lang="fr">
|
||||
<html lang="fr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${esc(subject)}</title>
|
||||
<!--[if mso]>
|
||||
<xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml>
|
||||
<style>table { border-collapse:collapse; } td { font-family:Arial,Helvetica,sans-serif; }</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#F2EEE8;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#F2EEE8;">
|
||||
<tr><td align="center" style="padding:36px 14px;">
|
||||
<body bgcolor="#F2EEE8" style="margin:0;padding:0;background-color:#F2EEE8;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;">
|
||||
<table id="sbc-email-root" role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#F2EEE8" style="width:100%;background-color:#F2EEE8;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;">
|
||||
<tr><td align="center" valign="top" style="padding:36px 14px;">
|
||||
|
||||
<table role="presentation" width="620" cellpadding="0" cellspacing="0" border="0" style="max-width:620px;width:100%;background-color:#FFFFFF;border:1px solid #E7E2DB;border-radius:8px;overflow:hidden;">
|
||||
<!--[if mso]><table role="presentation" width="620" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#FFFFFF" style="width:100%;max-width:620px;background-color:#FFFFFF;border:1px solid #E7E2DB;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;">
|
||||
|
||||
<tr><td style="background-color:#161514;padding:26px 40px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<tr><td bgcolor="#161514" style="background-color:#161514;padding:26px 40px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr>
|
||||
<td width="52" valign="middle">
|
||||
<img src="${esc(logoUrl)}" width="52" height="52" alt="SLUC Business Club Nancy" style="display:block;border-radius:6px;background-color:#FFFFFF;">
|
||||
</td>
|
||||
@@ -108,17 +118,17 @@ export function buildInvitationEmail({ rencontre, baseUrl, texts = {} }) {
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="padding:40px 40px 12px;">
|
||||
<div style="font-family:Arial,Helvetica,sans-serif;font-size:11px;letter-spacing:2.5px;text-transform:uppercase;color:#C1272D;font-weight:bold;padding-bottom:18px;">Invitation · Rencontre</div>
|
||||
${t.greeting ? `<div style="font-family:Georgia,'Times New Roman',serif;font-size:19px;color:#1B1B1B;padding-bottom:14px;">${escML(t.greeting)}</div>` : ''}
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td style="padding-bottom:18px;font-family:Arial,Helvetica,sans-serif;font-size:11px;line-height:14px;letter-spacing:2.5px;text-transform:uppercase;color:#C1272D;font-weight:bold;">Invitation · Rencontre</td></tr></table>
|
||||
${t.greeting ? `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td style="padding-bottom:14px;font-family:Georgia,'Times New Roman',serif;font-size:19px;line-height:23px;color:#1B1B1B;">${escML(t.greeting)}</td></tr></table>` : ''}
|
||||
${para(t.intro, 'padding-bottom:26px;')}
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="padding:0 40px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#FAF8F5;border:1px solid #E7E2DB;border-left:3px solid #C1272D;border-radius:4px;">
|
||||
<tr><td style="padding:26px 28px;">
|
||||
<div style="font-family:Georgia,'Times New Roman',serif;font-size:24px;line-height:1.2;color:#1B1B1B;padding-bottom:12px;">${esc(r.titre)}</div>
|
||||
${r.description ? `<div style="font-family:Arial,Helvetica,sans-serif;font-size:14.5px;line-height:1.65;color:#5A544E;padding-bottom:16px;">${esc(r.description)}</div>` : ''}
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#FAF8F5" style="width:100%;background-color:#FAF8F5;border:1px solid #E7E2DB;border-left:3px solid #C1272D;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;">
|
||||
<tr><td bgcolor="#FAF8F5" style="padding:26px 28px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td style="padding-bottom:12px;font-family:Georgia,'Times New Roman',serif;font-size:24px;line-height:29px;color:#1B1B1B;">${esc(r.titre)}</td></tr></table>
|
||||
${r.description ? `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td style="padding-bottom:16px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:23px;mso-line-height-rule:exactly;color:#5A544E;">${esc(r.description)}</td></tr></table>` : ''}
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr>
|
||||
<td style="border-top:1px solid #E7E2DB;padding-top:16px;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#5A544E;line-height:1.9;">
|
||||
${infoLines.join('<br>')}
|
||||
</td>
|
||||
@@ -128,30 +138,31 @@ export function buildInvitationEmail({ rencontre, baseUrl, texts = {} }) {
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="padding:6px 40px 0;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr>
|
||||
<td align="center" style="padding:28px 0 0;">
|
||||
${ctaButton(inscriptionUrl, "Je m'inscris à la rencontre")}
|
||||
</td>
|
||||
</tr></table>
|
||||
<div style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#9A938B;text-align:center;padding-top:14px;line-height:1.6;">Réservé aux membres à jour de leur adhésion pour la saison en cours.</div>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td align="center" style="padding-top:14px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#9A938B;">Réservé aux membres à jour de leur adhésion pour la saison en cours.</td></tr></table>
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="padding:28px 40px 38px;">
|
||||
${para(t.outro, t.signature ? 'padding-bottom:24px;' : '')}
|
||||
${t.signature ? `<div style="font-family:Georgia,'Times New Roman',serif;font-size:16px;line-height:1.6;color:#1B1B1B;">${escML(t.signature)}</div>` : ''}
|
||||
${t.signature ? `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td style="font-family:Georgia,'Times New Roman',serif;font-size:16px;line-height:26px;color:#1B1B1B;">${escML(t.signature)}</td></tr></table>` : ''}
|
||||
</td></tr>
|
||||
|
||||
<tr><td style="background-color:#0F0E0D;padding:24px 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#B7AFA6;line-height:1.8;">
|
||||
<tr><td bgcolor="#0F0E0D" style="background-color:#0F0E0D;padding:24px 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#B7AFA6;line-height:22px;">
|
||||
Business Club SLUC Nancy · Palais des Sports Jean Weille, Nancy<br>
|
||||
<a href="mailto:contact@sluc-businessclub.fr" style="color:#E0777A;text-decoration:none;">contact@sluc-businessclub.fr</a>
|
||||
</td></tr>
|
||||
|
||||
</table>
|
||||
|
||||
<div style="font-family:Arial,Helvetica,sans-serif;font-size:11px;color:#9A938B;padding-top:18px;max-width:620px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:620px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;"><tr><td style="padding-top:18px;font-family:Arial,Helvetica,sans-serif;font-size:11px;line-height:17px;color:#9A938B;">
|
||||
Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :<br>
|
||||
<a href="${esc(inscriptionUrl)}" style="color:#C1272D;text-decoration:underline;word-break:break-all;">${esc(inscriptionUrl)}</a>
|
||||
</div>
|
||||
</td></tr></table>
|
||||
<!--[if mso]></td></tr></table><![endif]-->
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -41,7 +41,8 @@ const MEMBER_SQL = `
|
||||
LEFT JOIN users u ON u.member_id = m.id`;
|
||||
|
||||
const RENC_SQL = `
|
||||
SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places, r.image_path,
|
||||
SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places,
|
||||
r.participants_par_compte, r.image_path,
|
||||
COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id`;
|
||||
|
||||
@@ -174,9 +175,9 @@ adminRouter.post('/rencontres', validate(rencontreSchema), async (req, res, next
|
||||
try {
|
||||
const d = req.data;
|
||||
const result = await query(
|
||||
`INSERT INTO rencontres (titre, date_renc, heure, lieu, description, places)
|
||||
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places]
|
||||
`INSERT INTO rencontres (titre, date_renc, heure, lieu, description, places, participants_par_compte)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places, d.participants_par_compte]
|
||||
);
|
||||
res.status(201).json({ id: result.rows[0].id });
|
||||
} catch (err) {
|
||||
@@ -188,9 +189,11 @@ adminRouter.put('/rencontres/:id', validate(idParam, 'params'), validate(rencont
|
||||
try {
|
||||
const d = req.data;
|
||||
const result = await query(
|
||||
`UPDATE rencontres SET titre=$1, date_renc=$2, heure=$3, lieu=$4, description=$5, places=$6
|
||||
WHERE id=$7 RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places, req.params.id]
|
||||
`UPDATE rencontres SET titre=$1, date_renc=$2, heure=$3, lieu=$4, description=$5,
|
||||
places=$6, participants_par_compte=$7
|
||||
WHERE id=$8 RETURNING id`,
|
||||
[d.titre, d.date_renc, d.heure, d.lieu, d.description, d.places,
|
||||
d.participants_par_compte, req.params.id]
|
||||
);
|
||||
if (result.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
res.json({ ok: true });
|
||||
|
||||
+133
-32
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { query } from '../db.js';
|
||||
import { pool, query } from '../db.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { validate } from '../middleware/validate.js';
|
||||
import { publicFormLimiter } from '../middleware/security.js';
|
||||
import { demandeSchema, inscriptionPublicSchema, idParam } from '../schemas.js';
|
||||
@@ -19,7 +20,8 @@ publicRouter.get('/bootstrap', async (_req, res, next) => {
|
||||
WHERE m.valide = true ORDER BY m.nom`
|
||||
),
|
||||
query(
|
||||
`SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places, r.image_path,
|
||||
`SELECT r.id, r.titre, r.date_renc, r.heure, r.lieu, r.description, r.places,
|
||||
r.participants_par_compte, r.image_path,
|
||||
COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id
|
||||
WHERE r.date_renc >= CURRENT_DATE
|
||||
@@ -73,42 +75,141 @@ publicRouter.post(
|
||||
}
|
||||
);
|
||||
|
||||
// Event registration (public modal)
|
||||
publicRouter.post(
|
||||
'/rencontres/:id/inscriptions',
|
||||
publicFormLimiter,
|
||||
// Registration context for the connected member account. Existing names are
|
||||
// returned so the member can review or amend the complete participant list.
|
||||
publicRouter.get(
|
||||
'/rencontres/:id/inscription',
|
||||
requireAuth('member'),
|
||||
validate(idParam, 'params'),
|
||||
validate(inscriptionPublicSchema),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { nom, entreprise, email, tel } = req.data;
|
||||
const renc = await query(
|
||||
`SELECT r.places, COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id
|
||||
WHERE r.id = $1 AND r.date_renc >= CURRENT_DATE
|
||||
GROUP BY r.id`,
|
||||
[id]
|
||||
);
|
||||
if (renc.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
if (renc.rows[0].inscrits >= renc.rows[0].places) {
|
||||
return res.status(409).json({ error: 'Cette rencontre est complète.' });
|
||||
const [member, rencontre, participants] = await Promise.all([
|
||||
query('SELECT id, nom, dirigeant, email, tel, valide FROM members WHERE id = $1', [req.user.memberId]),
|
||||
query(
|
||||
`SELECT r.id, r.places, r.participants_par_compte, COUNT(i.id)::int AS inscrits
|
||||
FROM rencontres r LEFT JOIN inscriptions i ON i.rencontre_id = r.id
|
||||
WHERE r.id = $1 AND r.date_renc >= CURRENT_DATE
|
||||
GROUP BY r.id`,
|
||||
[req.params.id]
|
||||
),
|
||||
query(
|
||||
`SELECT nom, statut FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND member_id = $2 ORDER BY created_at, id`,
|
||||
[req.params.id, req.user.memberId]
|
||||
),
|
||||
]);
|
||||
if (member.rowCount === 0) return res.status(403).json({ error: 'Compte membre introuvable.' });
|
||||
if (!member.rows[0].valide) {
|
||||
return res.status(403).json({ error: "Votre adhésion doit être validée pour vous inscrire." });
|
||||
}
|
||||
const dup = await query(
|
||||
'SELECT 1 FROM inscriptions WHERE rencontre_id = $1 AND email = $2',
|
||||
[id, email]
|
||||
);
|
||||
if (dup.rowCount > 0) {
|
||||
return res.status(409).json({ error: 'Une inscription existe déjà avec cet email.' });
|
||||
}
|
||||
await query(
|
||||
`INSERT INTO inscriptions (rencontre_id, nom, entreprise, email, tel, statut)
|
||||
VALUES ($1, $2, $3, $4, $5, 'en_attente')`,
|
||||
[id, nom, entreprise, email, tel]
|
||||
);
|
||||
res.status(201).json({ ok: true });
|
||||
if (rencontre.rowCount === 0) return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
res.json({ member: member.rows[0], rencontre: rencontre.rows[0], participants: participants.rows });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// One authenticated member account can register several named people, within
|
||||
// both the per-account limit and the event's total capacity.
|
||||
publicRouter.post(
|
||||
'/rencontres/:id/inscriptions',
|
||||
publicFormLimiter,
|
||||
requireAuth('member'),
|
||||
validate(idParam, 'params'),
|
||||
validate(inscriptionPublicSchema),
|
||||
async (req, res, next) => {
|
||||
let client;
|
||||
try {
|
||||
client = await pool.connect();
|
||||
const { id } = req.params;
|
||||
const participants = req.data.participants;
|
||||
const names = participants.map((name) => name.toLocaleLowerCase('fr-FR'));
|
||||
if (new Set(names).size !== names.length) {
|
||||
return res.status(400).json({ error: 'Chaque participant doit avoir un nom différent.' });
|
||||
}
|
||||
|
||||
await client.query('BEGIN');
|
||||
// Lock the event before counting places so concurrent registrations
|
||||
// cannot overbook it.
|
||||
const renc = await client.query(
|
||||
`SELECT id, places, participants_par_compte FROM rencontres
|
||||
WHERE id = $1 AND date_renc >= CURRENT_DATE FOR UPDATE`,
|
||||
[id]
|
||||
);
|
||||
if (renc.rowCount === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ error: 'Rencontre introuvable' });
|
||||
}
|
||||
const member = await client.query(
|
||||
'SELECT id, nom, email, tel, valide FROM members WHERE id = $1',
|
||||
[req.user.memberId]
|
||||
);
|
||||
if (member.rowCount === 0 || !member.rows[0].valide) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(403).json({ error: "Votre adhésion doit être validée pour vous inscrire." });
|
||||
}
|
||||
if (participants.length > renc.rows[0].participants_par_compte) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(409).json({
|
||||
error: `Cette rencontre autorise ${renc.rows[0].participants_par_compte} participant(s) maximum par compte.`,
|
||||
});
|
||||
}
|
||||
|
||||
const otherCount = await client.query(
|
||||
`SELECT COUNT(*)::int AS n FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND (member_id IS NULL OR member_id <> $2)`,
|
||||
[id, req.user.memberId]
|
||||
);
|
||||
if (otherCount.rows[0].n + participants.length > renc.rows[0].places) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(409).json({ error: 'Il ne reste pas assez de places pour ces participants.' });
|
||||
}
|
||||
|
||||
const existing = await client.query(
|
||||
`SELECT id, nom FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND member_id = $2 ORDER BY created_at, id`,
|
||||
[id, req.user.memberId]
|
||||
);
|
||||
const available = new Map();
|
||||
for (const row of existing.rows) {
|
||||
const key = row.nom.toLocaleLowerCase('fr-FR');
|
||||
if (!available.has(key)) available.set(key, []);
|
||||
available.get(key).push(row);
|
||||
}
|
||||
|
||||
const keptIds = [];
|
||||
const account = member.rows[0];
|
||||
for (const nom of participants) {
|
||||
const key = nom.toLocaleLowerCase('fr-FR');
|
||||
const row = available.get(key)?.shift();
|
||||
if (row) {
|
||||
keptIds.push(row.id);
|
||||
await client.query(
|
||||
'UPDATE inscriptions SET nom=$1, entreprise=$2, email=$3, tel=$4 WHERE id=$5',
|
||||
[nom, account.nom, account.email, account.tel, row.id]
|
||||
);
|
||||
} else {
|
||||
const inserted = await client.query(
|
||||
`INSERT INTO inscriptions (rencontre_id, member_id, nom, entreprise, email, tel, statut)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'en_attente') RETURNING id`,
|
||||
[id, account.id, nom, account.nom, account.email, account.tel]
|
||||
);
|
||||
keptIds.push(inserted.rows[0].id);
|
||||
}
|
||||
}
|
||||
await client.query(
|
||||
`DELETE FROM inscriptions
|
||||
WHERE rencontre_id = $1 AND member_id = $2 AND NOT (id = ANY($3::int[]))`,
|
||||
[id, account.id, keptIds]
|
||||
);
|
||||
await client.query('COMMIT');
|
||||
res.status(201).json({ ok: true, participants: participants.length });
|
||||
} catch (err) {
|
||||
if (client) await client.query('ROLLBACK').catch(() => {});
|
||||
next(err);
|
||||
} finally {
|
||||
client?.release();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -27,10 +27,7 @@ export const demandeSchema = z.object({
|
||||
});
|
||||
|
||||
export const inscriptionPublicSchema = z.object({
|
||||
nom: trimmed(120, 1),
|
||||
entreprise: trimmed(120, 1),
|
||||
email: trimmed(254, 3).email(),
|
||||
tel: trimmed(30).optional().default(''),
|
||||
participants: z.array(trimmed(120, 1)).min(1).max(100),
|
||||
});
|
||||
|
||||
export const memberProfileSchema = z.object({
|
||||
@@ -55,6 +52,7 @@ export const rencontreSchema = z.object({
|
||||
lieu: trimmed(200).optional().default(''),
|
||||
description: trimmed(2000).optional().default(''),
|
||||
places: z.coerce.number().int().min(0).max(100000),
|
||||
participants_par_compte: z.coerce.number().int().min(1).max(100).optional().default(1),
|
||||
});
|
||||
|
||||
export const inscriptionAdminSchema = z.object({
|
||||
|
||||
@@ -56,14 +56,20 @@ CREATE TABLE IF NOT EXISTS rencontres (
|
||||
lieu TEXT NOT NULL DEFAULT '' CHECK (char_length(lieu) <= 200),
|
||||
description TEXT NOT NULL DEFAULT '' CHECK (char_length(description) <= 2000),
|
||||
places INTEGER NOT NULL CHECK (places >= 0 AND places <= 100000),
|
||||
participants_par_compte INTEGER NOT NULL DEFAULT 1 CHECK (participants_par_compte BETWEEN 1 AND 100),
|
||||
image_path TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE rencontres ADD COLUMN IF NOT EXISTS image_path TEXT;
|
||||
ALTER TABLE rencontres ADD COLUMN IF NOT EXISTS participants_par_compte INTEGER NOT NULL DEFAULT 1;
|
||||
ALTER TABLE rencontres DROP CONSTRAINT IF EXISTS rencontres_participants_par_compte_check;
|
||||
ALTER TABLE rencontres ADD CONSTRAINT rencontres_participants_par_compte_check
|
||||
CHECK (participants_par_compte BETWEEN 1 AND 100);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inscriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
rencontre_id INTEGER NOT NULL REFERENCES rencontres(id) ON DELETE CASCADE,
|
||||
member_id INTEGER REFERENCES members(id) ON DELETE SET NULL,
|
||||
nom TEXT NOT NULL CHECK (char_length(nom) BETWEEN 1 AND 120),
|
||||
entreprise TEXT NOT NULL CHECK (char_length(entreprise) BETWEEN 1 AND 120),
|
||||
email CITEXT CHECK (char_length(email) <= 254),
|
||||
@@ -72,6 +78,12 @@ CREATE TABLE IF NOT EXISTS inscriptions (
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_inscriptions_rencontre ON inscriptions(rencontre_id);
|
||||
ALTER TABLE inscriptions ADD COLUMN IF NOT EXISTS member_id INTEGER REFERENCES members(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_inscriptions_member_rencontre ON inscriptions(member_id, rencontre_id);
|
||||
-- Attach legacy registrations when their email identifies a member account.
|
||||
UPDATE inscriptions i SET member_id = m.id
|
||||
FROM members m
|
||||
WHERE i.member_id IS NULL AND i.email IS NOT NULL AND i.email = m.email;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rencontres_passees (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
Reference in new issue
Block a user