nodebb-plugin-calendar-onekite 12.0.5 → 12.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/admin.js CHANGED
@@ -4,18 +4,85 @@ const meta = require.main.require('./src/meta');
4
4
  const user = require.main.require('./src/user');
5
5
  const emailer = require.main.require('./src/emailer');
6
6
  const nconf = require.main.require('nconf');
7
- const { sendTemplateEmail } = require('./utils/mailer');
8
- const { formatFR } = require('./utils/time');
9
- const { normalizeReturnUrl, normalizeCallbackUrl } = require('./utils/url');
10
7
 
11
8
  function forumBaseUrl() {
12
9
  const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
13
10
  return base;
14
11
  }
15
12
 
13
+ function formatFR(tsOrIso) {
14
+ const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
15
+ const dd = String(d.getDate()).padStart(2, '0');
16
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
17
+ const yyyy = d.getFullYear();
18
+ return `${dd}/${mm}/${yyyy}`;
19
+ }
20
+
16
21
  async function sendEmail(template, toEmail, subject, data) {
22
+ // Prefer sending by uid (NodeBB core expects uid in various places)
17
23
  const uid = data && Number.isInteger(data.uid) ? data.uid : null;
18
- await sendTemplateEmail({ template, uid, toEmail, subject, data });
24
+ if (!toEmail && !uid) return;
25
+
26
+ const settings = await meta.settings.get('calendar-onekite').catch(() => ({}));
27
+ const lang = (settings && settings.defaultLang) || (meta && meta.config && meta.config.defaultLang) || 'fr';
28
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
29
+
30
+ // If we have a uid, use the native uid-based sender first.
31
+ try {
32
+ if (uid && typeof emailer.send === 'function') {
33
+ // NodeBB: send(template, uid, params)
34
+ if (emailer.send.length >= 3) {
35
+ await emailer.send(template, uid, params);
36
+ } else {
37
+ await emailer.send(template, uid, params);
38
+ }
39
+ return;
40
+ }
41
+ } catch (err) {
42
+ console.warn('[calendar-onekite] Failed to send email', {
43
+ template,
44
+ toEmail,
45
+ err: err && err.message ? err.message : String(err),
46
+ });
47
+ }
48
+
49
+ try {
50
+ if (typeof emailer.sendToEmail === 'function') {
51
+ // NodeBB: sendToEmail(template, email, language, params)
52
+ if (emailer.sendToEmail.length >= 4) {
53
+ await emailer.sendToEmail(template, toEmail, lang, params);
54
+ } else {
55
+ // Older signature: sendToEmail(template, email, params)
56
+ await emailer.sendToEmail(template, toEmail, params);
57
+ }
58
+ return;
59
+ }
60
+ } catch (err) {
61
+ console.warn('[calendar-onekite] Failed to send email', {
62
+ template,
63
+ toEmail,
64
+ err: err && err.message ? err.message : String(err),
65
+ });
66
+ }
67
+ }
68
+
69
+ function normalizeCallbackUrl(configured, meta) {
70
+ const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
71
+ let url = (configured || '').trim();
72
+ if (!url) {
73
+ url = base ? `${base}/helloasso` : '';
74
+ }
75
+ if (url && url.startsWith('/') && base) {
76
+ url = `${base}${url}`;
77
+ }
78
+ return url;
79
+ }
80
+
81
+ function normalizeReturnUrl(meta) {
82
+ const base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
83
+ const b = String(base || '').trim().replace(/\/$/, '');
84
+ if (!b) return '';
85
+ return `${b}/calendar`;
19
86
  }
20
87
 
21
88
 
@@ -45,13 +112,10 @@ admin.saveSettings = async function (req, res) {
45
112
  admin.listPending = async function (req, res) {
46
113
  const ids = await dbLayer.listAllReservationIds(5000);
47
114
  const pending = [];
48
- // Bulk fetch in chunks to avoid long key lists
49
- const chunkSize = 500;
50
- for (let i = 0; i < ids.length; i += chunkSize) {
51
- const slice = ids.slice(i, i + chunkSize);
52
- const rows = await dbLayer.getReservations(slice);
53
- for (const r of rows) {
54
- if (r && r.status === 'pending') pending.push(r);
115
+ for (const rid of ids) {
116
+ const r = await dbLayer.getReservation(rid);
117
+ if (r && r.status === 'pending') {
118
+ pending.push(r);
55
119
  }
56
120
  }
57
121
  pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
@@ -202,20 +266,15 @@ admin.purgeByYear = async function (req, res) {
202
266
  const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
203
267
  const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
204
268
 
205
- // IMPORTANT:
206
- // Purging "locations" (reservations) must NOT wipe accounting.
207
- // Accounting is computed from paid reservations, so for paid ones we keep the
208
- // reservation object but mark it as hidden from the calendar.
209
- // Non-paid reservations can be safely removed.
210
269
  const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 100000);
211
- const ts = Date.now();
212
270
  let removed = 0;
213
271
  let archivedForAccounting = 0;
214
-
272
+ const ts = Date.now();
215
273
  for (const rid of ids) {
216
274
  const r = await dbLayer.getReservation(rid);
217
275
  if (!r) continue;
218
276
 
277
+ // Keep paid reservations for accounting; just hide them from the calendar.
219
278
  if (String(r.status) === 'paid') {
220
279
  if (!r.calendarPurgedAt) {
221
280
  r.calendarPurgedAt = ts;
@@ -228,7 +287,6 @@ admin.purgeByYear = async function (req, res) {
228
287
  await dbLayer.removeReservation(rid);
229
288
  removed++;
230
289
  }
231
-
232
290
  res.json({ ok: true, removed, archivedForAccounting });
233
291
  };
234
292
 
package/lib/api.js CHANGED
@@ -9,23 +9,82 @@ const user = require.main.require('./src/user');
9
9
  const groups = require.main.require('./src/groups');
10
10
 
11
11
  const dbLayer = require('./db');
12
+
13
+ async function userInAnyGroup(uid, allowed) {
14
+ if (!uid || !allowed || !allowed.length) return false;
15
+ const ug = await groups.getUserGroups([uid]);
16
+ const slugs = ug[0] || [];
17
+ return allowed.some(g => slugs.includes(g));
18
+ }
19
+
12
20
  const helloasso = require('./helloasso');
13
- const { sendTemplateEmail } = require('./utils/mailer');
14
- const urlUtils = require('./utils/url');
15
- const timeUtils = require('./utils/time');
16
- const permUtils = require('./utils/permissions');
17
21
 
18
22
  // Email helper: NodeBB's Emailer signature differs across versions.
19
23
  // We try the common forms. Any failure is logged for debugging.
20
24
  async function sendEmail(template, toEmail, subject, data) {
21
- await sendTemplateEmail({ template, toEmail, subject, data });
25
+ if (!toEmail) return;
26
+ try {
27
+ // NodeBB core signature (historically):
28
+ // Emailer.sendToEmail(template, email, language, params[, callback])
29
+ // Subject is not a positional arg; it must be injected (either by NodeBB itself
30
+ // or via filter:email.modify). We always pass it in params.subject.
31
+ const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
32
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
33
+ if (typeof emailer.sendToEmail === 'function') {
34
+ await emailer.sendToEmail(template, toEmail, language, params);
35
+ return;
36
+ }
37
+ // Fallback for older/unusual builds (rare)
38
+ if (typeof emailer.send === 'function') {
39
+ // Some builds accept (template, email, language, params)
40
+ if (emailer.send.length >= 4) {
41
+ await emailer.send(template, toEmail, language, params);
42
+ return;
43
+ }
44
+ // Some builds accept (template, email, params)
45
+ await emailer.send(template, toEmail, params);
46
+ }
47
+ } catch (err) {
48
+ // eslint-disable-next-line no-console
49
+ console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
50
+ }
22
51
  }
23
52
 
24
- function normalizeBaseUrl(meta) { return urlUtils.normalizeBaseUrl(meta); }
53
+ function normalizeBaseUrl(meta) {
54
+ // Prefer meta.config.url, fallback to nconf.get('url')
55
+ let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
56
+ if (!base) {
57
+ base = String(nconf.get('url') || '').trim();
58
+ }
59
+ base = String(base || '').trim().replace(/\/$/, '');
60
+ // Ensure absolute with scheme
61
+ if (base && !/^https?:\/\//i.test(base)) {
62
+ base = `https://${base.replace(/^\/\//, '')}`;
63
+ }
64
+ return base;
65
+ }
25
66
 
26
- function normalizeCallbackUrl(configured, meta) { return urlUtils.normalizeCallbackUrl(configured, meta); }
67
+ function normalizeCallbackUrl(configured, meta) {
68
+ const base = normalizeBaseUrl(meta);
69
+ let url = (configured || '').trim();
70
+ if (!url) {
71
+ // Default webhook endpoint (recommended): namespaced under /plugins
72
+ url = base ? `${base}/plugins/calendar-onekite/helloasso` : '';
73
+ }
74
+ if (url && url.startsWith('/') && base) {
75
+ url = `${base}${url}`;
76
+ }
77
+ // Ensure scheme for absolute URLs
78
+ if (url && !/^https?:\/\//i.test(url)) {
79
+ url = `https://${url.replace(/^\/\//, '')}`;
80
+ }
81
+ return url;
82
+ }
27
83
 
28
- function normalizeReturnUrl(meta) { return urlUtils.normalizeReturnUrl(meta); }
84
+ function normalizeReturnUrl(meta) {
85
+ const base = normalizeBaseUrl(meta);
86
+ return base ? `${base}/calendar` : '';
87
+ }
29
88
 
30
89
 
31
90
  function overlap(aStart, aEnd, bStart, bEnd) {
@@ -33,32 +92,100 @@ function overlap(aStart, aEnd, bStart, bEnd) {
33
92
  }
34
93
 
35
94
 
36
- function formatFR(tsOrIso) { return timeUtils.formatFR(tsOrIso); }
95
+ function formatFR(tsOrIso) {
96
+ const d = new Date(typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso);
97
+ const dd = String(d.getDate()).padStart(2, '0');
98
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
99
+ const yyyy = d.getFullYear();
100
+ return `${dd}/${mm}/${yyyy}`;
101
+ }
37
102
 
38
- function toTs(v) { return timeUtils.toTs(v); }
103
+ function toTs(v) {
104
+ if (v === undefined || v === null || v === '') return NaN;
105
+ // Accept milliseconds timestamps passed as strings or numbers.
106
+ if (typeof v === 'number') return v;
107
+ const s = String(v).trim();
108
+ if (/^[0-9]+$/.test(s)) return parseInt(s, 10);
109
+ const d = new Date(s);
110
+ return d.getTime();
111
+ }
39
112
 
40
- function yearFromTs(ts) { return timeUtils.yearFromTs(ts); }
113
+ function yearFromTs(ts) {
114
+ const d = new Date(Number(ts));
115
+ return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
116
+ }
41
117
 
42
- function autoCreatorGroupForYear(year) { return permUtils.autoCreatorGroupForYear(year); }
118
+ function autoCreatorGroupForYear(year) {
119
+ return `onekite-ffvl-${year}`;
120
+ }
43
121
 
44
122
  function autoFormSlugForYear(year) {
45
123
  return `locations-materiel-${year}`;
46
124
  }
47
125
 
48
126
  async function canRequest(uid, settings, startTs) {
49
- return await permUtils.canRequest(uid, settings, startTs);
127
+ const year = yearFromTs(startTs);
128
+ const defaultGroup = autoCreatorGroupForYear(year);
129
+ const extras = (settings.creatorGroups || settings.allowedGroups || '').split(',').map(s => s.trim()).filter(Boolean);
130
+ const allowed = [defaultGroup, ...extras].filter(Boolean);
131
+ // If only the default group exists, enforce membership (do not open access to all).
132
+ if (!allowed.length) return true;
133
+ for (const g of allowed) {
134
+ const isMember = await groups.isMember(uid, g);
135
+ if (isMember) return true;
136
+ }
137
+ return false;
50
138
  }
51
139
 
52
140
  async function canValidate(uid, settings) {
53
- return await permUtils.canValidate(uid, settings);
141
+ // Always allow forum administrators (and global moderators) to validate,
142
+ // even if validatorGroups is empty.
143
+ try {
144
+ const isAdmin = await groups.isMember(uid, 'administrators');
145
+ if (isAdmin) return true;
146
+ const isGlobalMod = await groups.isMember(uid, 'Global Moderators');
147
+ if (isGlobalMod) return true;
148
+ } catch (e) {}
149
+
150
+ const allowed = (settings.validatorGroups || '').split(',').map(s => s.trim()).filter(Boolean);
151
+ if (!allowed.length) return false;
152
+ for (const g of allowed) {
153
+ const isMember = await groups.isMember(uid, g);
154
+ if (isMember) return true;
155
+ }
156
+ return false;
54
157
  }
55
158
 
56
159
  async function canCreateSpecial(uid, settings) {
57
- return await permUtils.canCreateSpecial(uid, settings);
160
+ if (!uid) return false;
161
+ try {
162
+ const isAdmin = await groups.isMember(uid, 'administrators');
163
+ if (isAdmin) return true;
164
+ const isGlobalMod = await groups.isMember(uid, 'Global Moderators');
165
+ if (isGlobalMod) return true;
166
+ } catch (e) {}
167
+ const allowed = (settings.specialCreatorGroups || '').split(',').map(s => s.trim()).filter(Boolean);
168
+ if (!allowed.length) return false;
169
+ for (const g of allowed) {
170
+ if (await groups.isMember(uid, g)) return true;
171
+ }
172
+ return false;
58
173
  }
59
174
 
60
175
  async function canDeleteSpecial(uid, settings) {
61
- return await permUtils.canDeleteSpecial(uid, settings);
176
+ if (!uid) return false;
177
+ try {
178
+ const isAdmin = await groups.isMember(uid, 'administrators');
179
+ if (isAdmin) return true;
180
+ const isGlobalMod = await groups.isMember(uid, 'Global Moderators');
181
+ if (isGlobalMod) return true;
182
+ } catch (e) {}
183
+ const allowed = (settings.specialDeleterGroups || settings.specialCreatorGroups || '').split(',').map(s => s.trim()).filter(Boolean);
184
+ if (!allowed.length) return false;
185
+ for (const g of allowed) {
186
+ if (await groups.isMember(uid, g)) return true;
187
+ }
188
+ return false;
62
189
  }
63
190
 
64
191
  function eventsFor(resv) {
@@ -156,8 +283,7 @@ api.getEvents = async function (req, res) {
156
283
  for (const rid of ids) {
157
284
  const r = await dbLayer.getReservation(rid);
158
285
  if (!r) continue;
159
- // If the reservation was purged from the calendar (but kept for accounting),
160
- // do not show it in the calendar UI.
286
+ // Purged from calendar view (kept for accounting)
161
287
  if (r.calendarPurgedAt) continue;
162
288
  // Only show active statuses
163
289
  if (!['pending', 'awaiting_payment', 'paid'].includes(r.status)) continue;
package/lib/db.js CHANGED
@@ -14,19 +14,6 @@ async function getReservation(rid) {
14
14
  return await db.getObject(KEY_OBJ(rid));
15
15
  }
16
16
 
17
-
18
- async function getReservations(rids) {
19
- if (!Array.isArray(rids) || !rids.length) return [];
20
- const keys = rids.map((rid) => KEY_OBJ(rid));
21
- return await db.getObjects(keys);
22
- }
23
-
24
- async function getSpecialEvents(eids) {
25
- if (!Array.isArray(eids) || !eids.length) return [];
26
- const keys = eids.map((eid) => KEY_SPECIAL_OBJ(eid));
27
- return await db.getObjects(keys);
28
- }
29
-
30
17
  async function saveReservation(resv) {
31
18
  await db.setObject(KEY_OBJ(resv.rid), resv);
32
19
  // score = start timestamp
@@ -63,8 +50,6 @@ module.exports = {
63
50
  KEY_SPECIAL_ZSET,
64
51
  KEY_CHECKOUT_INTENT_TO_RID,
65
52
  getReservation,
66
- getReservations,
67
- getSpecialEvents,
68
53
  saveReservation,
69
54
  removeReservation,
70
55
  // Special events
package/lib/scheduler.js CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  const meta = require.main.require('./src/meta');
4
4
  const dbLayer = require('./db');
5
- const { sendTemplateEmail } = require('./utils/mailer');
6
- const { formatFR } = require('./utils/time');
7
5
 
8
6
  let timer = null;
9
7
 
@@ -25,12 +23,8 @@ async function expirePending() {
25
23
  return;
26
24
  }
27
25
 
28
- const chunkSize = 500;
29
- for (let i = 0; i < ids.length; i += chunkSize) {
30
- const slice = ids.slice(i, i + chunkSize);
31
- const rows = await dbLayer.getReservations(slice);
32
- for (const resv of rows) {
33
- const rid = resv && resv.rid;
26
+ for (const rid of ids) {
27
+ const resv = await dbLayer.getReservation(rid);
34
28
  if (!resv || resv.status !== 'pending') {
35
29
  continue;
36
30
  }
@@ -40,7 +34,6 @@ async function expirePending() {
40
34
  // Expire (remove from calendar)
41
35
  await dbLayer.removeReservation(rid);
42
36
  }
43
- }
44
37
  }
45
38
  }
46
39
 
@@ -58,12 +51,55 @@ async function processAwaitingPayment() {
58
51
 
59
52
  const ids = await dbLayer.listAllReservationIds(5000);
60
53
  if (!ids || !ids.length) return;
61
- const user = require.main.require('./src/user'); const chunkSize = 500;
62
- for (let i = 0; i < ids.length; i += chunkSize) {
63
- const slice = ids.slice(i, i + chunkSize);
64
- const rows = await dbLayer.getReservations(slice);
65
- for (const r of rows) {
66
- const rid = r && r.rid;
54
+
55
+ const emailer = require.main.require('./src/emailer');
56
+ const user = require.main.require('./src/user');
57
+
58
+ async function sendEmail(template, toEmail, subject, data) {
59
+ if (!toEmail) return;
60
+ try {
61
+ // NodeBB core signature:
62
+ // Emailer.sendToEmail(template, email, language, params[, callback])
63
+ // Subject is NOT a positional argument; it must be provided in params.subject
64
+ // and optionally copied into the final email by filter:email.modify.
65
+ const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
66
+ const params = Object.assign({}, data || {}, subject ? { subject } : {});
67
+
68
+ if (typeof emailer.sendToEmail === 'function') {
69
+ await emailer.sendToEmail(template, toEmail, language, params);
70
+ return;
71
+ }
72
+
73
+ // Fallbacks for older/unusual builds
74
+ if (typeof emailer.send === 'function') {
75
+ // Some builds accept (template, email, language, params)
76
+ if (emailer.send.length >= 4) {
77
+ await emailer.send(template, toEmail, language, params);
78
+ return;
79
+ }
80
+ // Some builds accept (template, email, params)
81
+ await emailer.send(template, toEmail, params);
82
+ }
83
+ } catch (err) {
84
+ // eslint-disable-next-line no-console
85
+ console.warn('[calendar-onekite] Failed to send email (scheduler)', {
86
+ template,
87
+ toEmail,
88
+ err: String((err && err.message) || err),
89
+ });
90
+ }
91
+ }
92
+
93
+ function formatFR(ts) {
94
+ const d = new Date(ts);
95
+ const dd = String(d.getDate()).padStart(2, '0');
96
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
97
+ const yyyy = d.getFullYear();
98
+ return `${dd}/${mm}/${yyyy}`;
99
+ }
100
+
101
+ for (const rid of ids) {
102
+ const r = await dbLayer.getReservation(rid);
67
103
  if (!r || r.status !== 'awaiting_payment') continue;
68
104
 
69
105
  const approvedAt = parseInt(r.approvedAt || r.validatedAt || 0, 10) || 0;
@@ -76,7 +112,7 @@ async function processAwaitingPayment() {
76
112
  // Send reminder once
77
113
  const u = await user.getUserFields(r.uid, ['username', 'email']);
78
114
  if (u && u.email) {
79
- await sendTemplateEmail({ template: 'calendar-onekite_reminder', toEmail: u.email, subject: 'Location matériel - Rappel', data: {
115
+ await sendEmail('calendar-onekite_reminder', u.email, 'Location matériel - Rappel', {
80
116
  username: u.username,
81
117
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
82
118
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -84,7 +120,7 @@ async function processAwaitingPayment() {
84
120
  paymentUrl: r.paymentUrl || '',
85
121
  delayMinutes: holdMins,
86
122
  pickupLine: r.pickupTime ? (r.adminNote ? `${r.pickupTime} à ${r.adminNote}` : r.pickupTime) : '',
87
- }});
123
+ });
88
124
  }
89
125
  r.reminderSent = true;
90
126
  r.reminderAt = now;
@@ -96,17 +132,16 @@ async function processAwaitingPayment() {
96
132
  // Expire: remove reservation so it disappears from calendar and frees items
97
133
  const u = await user.getUserFields(r.uid, ['username', 'email']);
98
134
  if (u && u.email) {
99
- await sendTemplateEmail({ template: 'calendar-onekite_expired', toEmail: u.email, subject: 'Location matériel - Rappel', data: {
135
+ await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
100
136
  username: u.username,
101
137
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
102
138
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
103
139
  dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
104
140
  delayMinutes: holdMins,
105
- }});
141
+ });
106
142
  }
107
143
  await dbLayer.removeReservation(rid);
108
144
  }
109
- }
110
145
  }
111
146
  }
112
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "12.0.5",
3
+ "version": "12.0.7",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/admin.js CHANGED
@@ -636,10 +636,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
636
636
  if (!ok) return;
637
637
  try {
638
638
  const r = await purge(year);
639
- const removed = r.removed || 0;
640
- const archived = r.archivedForAccounting || 0;
641
- const extra = archived ? `, ${archived} archivée(s) (compta conservée)` : '';
642
- showAlert('success', `Purge OK (${removed} supprimée(s)${extra}).`);
639
+ showAlert('success', `Purge OK (${r.removed || 0} supprimées, ${r.archivedForAccounting || 0} archivées — compta conservée).`);
643
640
  await refreshPending();
644
641
  } catch (e) {
645
642
  showAlert('error', 'Purge impossible.');
package/public/client.js CHANGED
@@ -886,25 +886,6 @@ function toDatetimeLocalValue(date) {
886
886
  }
887
887
 
888
888
  async function init(selector) {
889
-
890
- const container = typeof selector === 'string' ? document.querySelector(selector) : selector;
891
- if (!container) return;
892
-
893
- // Prevent double-init (ajaxify + initial load, or return from payment)
894
- if (container.dataset && container.dataset.onekiteCalendarInit === '1') {
895
- // Still refresh the custom button label in case FC rerendered
896
- try { refreshDesktopModeButton(); } catch (e) {}
897
- return;
898
- }
899
- if (container.dataset) container.dataset.onekiteCalendarInit = '1';
900
-
901
- // If a previous instance exists (shouldn't, but happens in some navigation flows), destroy it.
902
- try {
903
- if (window.oneKiteCalendar && typeof window.oneKiteCalendar.destroy === 'function') {
904
- window.oneKiteCalendar.destroy();
905
- }
906
- window.oneKiteCalendar = null;
907
- } catch (e) {}
908
889
  const el = document.querySelector(selector);
909
890
  if (!el) {
910
891
  return;
@@ -1082,7 +1063,6 @@ try {
1082
1063
 
1083
1064
  const calendar = new FullCalendar.Calendar(el, {
1084
1065
  initialView: 'dayGridMonth',
1085
- windowResize: function () { try { refreshDesktopModeButton(); } catch (e) {} },
1086
1066
  height: 'auto',
1087
1067
  contentHeight: 'auto',
1088
1068
  aspectRatio: computeAspectRatio(),
@@ -1691,21 +1671,6 @@ try {
1691
1671
 
1692
1672
  calendar.render();
1693
1673
 
1694
- // Keep the custom button label stable even if FullCalendar rerenders the toolbar
1695
- try {
1696
- const toolbar = container.querySelector('.fc-toolbar');
1697
- if (toolbar && !toolbar.__oneKiteObserved) {
1698
- toolbar.__oneKiteObserved = true;
1699
- const mo = new MutationObserver(() => {
1700
- try { refreshDesktopModeButton(); } catch (e) {}
1701
- });
1702
- mo.observe(toolbar, { childList: true, subtree: true, characterData: true });
1703
- // Disconnect after a while to avoid permanent overhead
1704
- setTimeout(() => { try { mo.disconnect(); } catch (e) {} }, 15000);
1705
- }
1706
- } catch (e) {}
1707
-
1708
-
1709
1674
  refreshDesktopModeButton();
1710
1675
 
1711
1676
 
@@ -1800,7 +1765,7 @@ try {
1800
1765
  // Auto-init on /calendar when ajaxify finishes rendering.
1801
1766
  function autoInit(data) {
1802
1767
  try {
1803
- const tpl = (data && data.template && data.template.name) || (ajaxify && ajaxify.data && ajaxify.data.template && ajaxify.data.template.name) || '';
1768
+ const tpl = data && data.template ? data.template.name : (ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '');
1804
1769
  if (tpl === 'calendar-onekite') {
1805
1770
  init('#onekite-calendar');
1806
1771
  }
@@ -1,59 +0,0 @@
1
- 'use strict';
2
-
3
- const meta = require.main.require('./src/meta');
4
- const emailer = require.main.require('./src/emailer');
5
-
6
- /**
7
- * Send NodeBB email templates in a way that survives slight core signature differences.
8
- * - Prefer uid-based sending when a uid is available.
9
- * - Otherwise use sendToEmail.
10
- *
11
- * @param {object} opts
12
- * @param {string} opts.template
13
- * @param {number} [opts.uid]
14
- * @param {string} [opts.toEmail]
15
- * @param {string} [opts.subject]
16
- * @param {object} [opts.data]
17
- */
18
- async function sendTemplateEmail(opts) {
19
- const template = opts && opts.template;
20
- const uid = opts && Number.isInteger(opts.uid) ? opts.uid : null;
21
- const toEmail = opts && opts.toEmail ? String(opts.toEmail) : '';
22
- const subject = opts && opts.subject ? String(opts.subject) : '';
23
- const data = (opts && opts.data) || {};
24
-
25
- if (!template) return;
26
- if (!uid && !toEmail) return;
27
-
28
- const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
29
- const params = Object.assign({}, data, subject ? { subject } : {});
30
-
31
- // uid-first
32
- if (uid && typeof emailer.send === 'function') {
33
- try {
34
- // NodeBB: send(template, uid, params)
35
- await emailer.send(template, uid, params);
36
- return;
37
- } catch (err) {
38
- // fall through to email
39
- }
40
- }
41
-
42
- if (toEmail && typeof emailer.sendToEmail === 'function') {
43
- try {
44
- // NodeBB commonly: sendToEmail(template, email, language, params)
45
- await emailer.sendToEmail(template, toEmail, language, params);
46
- return;
47
- } catch (err) {
48
- // eslint-disable-next-line no-console
49
- console.warn('[calendar-onekite] Failed to send email', {
50
- template,
51
- toEmail,
52
- uid,
53
- err: err && err.message ? err.message : String(err),
54
- });
55
- }
56
- }
57
- }
58
-
59
- module.exports = { sendTemplateEmail };
@@ -1,73 +0,0 @@
1
- 'use strict';
2
-
3
- const groups = require.main.require('./src/groups');
4
- const { yearFromTs } = require('./time');
5
-
6
- function autoCreatorGroupForYear(year) {
7
- return `onekite-ffvl-${year}`;
8
- }
9
-
10
- async function isPrivileged(uid) {
11
- try {
12
- if (await groups.isMember(uid, 'administrators')) return true;
13
- if (await groups.isMember(uid, 'Global Moderators')) return true;
14
- } catch (e) {}
15
- return false;
16
- }
17
-
18
- function parseGroupList(s) {
19
- return String(s || '')
20
- .split(',')
21
- .map((x) => x.trim())
22
- .filter(Boolean);
23
- }
24
-
25
- async function canRequest(uid, settings, startTs) {
26
- const year = yearFromTs(startTs);
27
- const defaultGroup = autoCreatorGroupForYear(year);
28
- const extras = parseGroupList(settings && (settings.creatorGroups || settings.allowedGroups));
29
- const allowed = [defaultGroup, ...extras].filter(Boolean);
30
- if (!allowed.length) return true;
31
- for (const g of allowed) {
32
- if (await groups.isMember(uid, g)) return true;
33
- }
34
- return false;
35
- }
36
-
37
- async function canValidate(uid, settings) {
38
- if (!uid) return false;
39
- if (await isPrivileged(uid)) return true;
40
- const allowed = parseGroupList(settings && settings.validatorGroups);
41
- for (const g of allowed) {
42
- if (await groups.isMember(uid, g)) return true;
43
- }
44
- return false;
45
- }
46
-
47
- async function canCreateSpecial(uid, settings) {
48
- if (!uid) return false;
49
- if (await isPrivileged(uid)) return true;
50
- const allowed = parseGroupList(settings && settings.specialCreatorGroups);
51
- for (const g of allowed) {
52
- if (await groups.isMember(uid, g)) return true;
53
- }
54
- return false;
55
- }
56
-
57
- async function canDeleteSpecial(uid, settings) {
58
- if (!uid) return false;
59
- if (await isPrivileged(uid)) return true;
60
- const allowed = parseGroupList(settings && (settings.specialDeleterGroups || settings.specialCreatorGroups));
61
- for (const g of allowed) {
62
- if (await groups.isMember(uid, g)) return true;
63
- }
64
- return false;
65
- }
66
-
67
- module.exports = {
68
- autoCreatorGroupForYear,
69
- canRequest,
70
- canValidate,
71
- canCreateSpecial,
72
- canDeleteSpecial,
73
- };
package/lib/utils/time.js DELETED
@@ -1,32 +0,0 @@
1
- 'use strict';
2
-
3
- function toTs(v) {
4
- if (v === undefined || v === null || v === '') return NaN;
5
- // Accept milliseconds timestamps passed as strings or numbers.
6
- if (typeof v === 'number') return v;
7
- const s = String(v).trim();
8
- if (/^[0-9]+$/.test(s)) return parseInt(s, 10);
9
- const d = new Date(s);
10
- return d.getTime();
11
- }
12
-
13
- function formatFR(tsOrIso) {
14
- const d = new Date(
15
- typeof tsOrIso === 'string' && /^[0-9]+$/.test(tsOrIso) ? parseInt(tsOrIso, 10) : tsOrIso
16
- );
17
- const dd = String(d.getDate()).padStart(2, '0');
18
- const mm = String(d.getMonth() + 1).padStart(2, '0');
19
- const yyyy = d.getFullYear();
20
- return `${dd}/${mm}/${yyyy}`;
21
- }
22
-
23
- function yearFromTs(ts) {
24
- const d = new Date(Number(ts));
25
- return Number.isFinite(d.getTime()) ? d.getFullYear() : new Date().getFullYear();
26
- }
27
-
28
- module.exports = {
29
- toTs,
30
- formatFR,
31
- yearFromTs,
32
- };
package/lib/utils/url.js DELETED
@@ -1,36 +0,0 @@
1
- 'use strict';
2
-
3
- const nconf = require.main.require('nconf');
4
-
5
- function normalizeBaseUrl(meta) {
6
- // Prefer meta.config.url, fallback to nconf.get('url')
7
- let base = (meta && meta.config && (meta.config.url || meta.config['url']))
8
- ? (meta.config.url || meta.config['url'])
9
- : '';
10
- if (!base) base = String(nconf.get('url') || '').trim();
11
- base = String(base || '').trim().replace(/\/$/, '');
12
- if (base && !/^https?:\/\//i.test(base)) {
13
- base = `https://${base.replace(/^\/\//, '')}`;
14
- }
15
- return base;
16
- }
17
-
18
- function normalizeCallbackUrl(configured, meta, fallbackPath = '/plugins/calendar-onekite/helloasso') {
19
- const base = normalizeBaseUrl(meta);
20
- let url = String(configured || '').trim();
21
- if (!url) url = base ? `${base}${fallbackPath}` : '';
22
- if (url && url.startsWith('/') && base) url = `${base}${url}`;
23
- if (url && !/^https?:\/\//i.test(url)) url = `https://${url.replace(/^\/\//, '')}`;
24
- return url;
25
- }
26
-
27
- function normalizeReturnUrl(meta, path = '/calendar') {
28
- const base = normalizeBaseUrl(meta);
29
- return base ? `${base}${path}` : '';
30
- }
31
-
32
- module.exports = {
33
- normalizeBaseUrl,
34
- normalizeCallbackUrl,
35
- normalizeReturnUrl,
36
- };