nodebb-plugin-calendar-onekite 11.1.71 → 11.1.72

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
@@ -22,7 +22,12 @@ async function sendEmail(template, toEmail, subject, data) {
22
22
  if (!toEmail) return;
23
23
  try {
24
24
  if (typeof emailer.sendToEmail === 'function') {
25
- await emailer.sendToEmail(template, toEmail, subject, data);
25
+ if (emailer.sendToEmail.length >= 4) {
26
+ await emailer.sendToEmail(template, toEmail, subject, data);
27
+ } else {
28
+ const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
29
+ await emailer.sendToEmail(template, toEmail, dataWithSubject);
30
+ }
26
31
  return;
27
32
  }
28
33
  if (typeof emailer.send === 'function') {
@@ -227,6 +232,24 @@ admin.purgeByYear = async function (req, res) {
227
232
  res.json({ ok: true, removed: count });
228
233
  };
229
234
 
235
+ admin.purgeSpecialEventsByYear = async function (req, res) {
236
+ const year = (req.body && req.body.year ? String(req.body.year) : '').trim();
237
+ if (!/^\d{4}$/.test(year)) {
238
+ return res.status(400).json({ error: 'invalid-year' });
239
+ }
240
+ const y = parseInt(year, 10);
241
+ const startTs = new Date(Date.UTC(y, 0, 1)).getTime();
242
+ const endTs = new Date(Date.UTC(y + 1, 0, 1)).getTime() - 1;
243
+
244
+ const ids = await dbLayer.listSpecialIdsByStartRange(startTs, endTs, 100000);
245
+ let count = 0;
246
+ for (const eid of ids) {
247
+ await dbLayer.removeSpecialEvent(eid);
248
+ count++;
249
+ }
250
+ return res.json({ ok: true, removed: count });
251
+ };
252
+
230
253
  // Debug endpoint to validate HelloAsso connectivity and item loading
231
254
 
232
255
 
@@ -28,7 +28,15 @@ async function sendEmail(template, toEmail, subject, data) {
28
28
  const emailer = require.main.require('./src/emailer');
29
29
  try {
30
30
  if (typeof emailer.sendToEmail === 'function') {
31
- await emailer.sendToEmail(template, toEmail, subject, data);
31
+ // NodeBB versions differ:
32
+ // - sendToEmail(template, email, subject, data)
33
+ // - sendToEmail(template, email, data)
34
+ if (emailer.sendToEmail.length >= 4) {
35
+ await emailer.sendToEmail(template, toEmail, subject, data);
36
+ } else {
37
+ const dataWithSubject = Object.assign({}, data || {}, { subject });
38
+ await emailer.sendToEmail(template, toEmail, dataWithSubject);
39
+ }
32
40
  return;
33
41
  }
34
42
  if (typeof emailer.send === 'function') {
@@ -37,7 +45,8 @@ async function sendEmail(template, toEmail, subject, data) {
37
45
  return;
38
46
  }
39
47
  if (emailer.send.length === 3) {
40
- await emailer.send(template, toEmail, data);
48
+ const dataWithSubject = Object.assign({}, data || {}, { subject });
49
+ await emailer.send(template, toEmail, dataWithSubject);
41
50
  return;
42
51
  }
43
52
  await emailer.send(template, toEmail, subject, data);
@@ -185,31 +194,6 @@ function isConfirmedPayment(payload) {
185
194
  }
186
195
  }
187
196
 
188
- async function sendEmail(template, toEmail, subject, data) {
189
- if (!toEmail) return;
190
- const emailer = require.main.require('./src/emailer');
191
- try {
192
- if (typeof emailer.sendToEmail === 'function') {
193
- await emailer.sendToEmail(template, toEmail, subject, data);
194
- return;
195
- }
196
- if (typeof emailer.send === 'function') {
197
- if (emailer.send.length >= 4) {
198
- await emailer.send(template, toEmail, subject, data);
199
- return;
200
- }
201
- if (emailer.send.length === 3) {
202
- await emailer.send(template, toEmail, data);
203
- return;
204
- }
205
- await emailer.send(template, toEmail, subject, data);
206
- }
207
- } catch (err) {
208
- // eslint-disable-next-line no-console
209
- console.warn('[calendar-onekite] Failed to send email (webhook)', { template, toEmail, err: String(err && err.message || err) });
210
- }
211
- }
212
-
213
197
  function formatFR(tsOrIso) {
214
198
  const d = new Date(tsOrIso);
215
199
  const dd = String(d.getDate()).padStart(2, '0');
package/library.js CHANGED
@@ -110,6 +110,9 @@ Plugin.init = async function (params) {
110
110
  router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
111
111
  router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
112
112
  router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
113
+
114
+ // Purge special events by year
115
+ router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
113
116
  });
114
117
 
115
118
  // HelloAsso callback endpoint (hardened)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.71",
3
+ "version": "11.1.72",
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
@@ -419,6 +419,20 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
419
419
  });
420
420
  }
421
421
 
422
+ async function purgeSpecialEvents(year) {
423
+ try {
424
+ return await fetchJson('/api/v3/admin/plugins/calendar-onekite/special-events/purge', {
425
+ method: 'POST',
426
+ body: JSON.stringify({ year }),
427
+ });
428
+ } catch (e) {
429
+ return await fetchJson('/api/admin/plugins/calendar-onekite/special-events/purge', {
430
+ method: 'POST',
431
+ body: JSON.stringify({ year }),
432
+ });
433
+ }
434
+ }
435
+
422
436
  async function debugHelloAsso() {
423
437
  try {
424
438
  return await fetchJson('/api/v3/admin/plugins/calendar-onekite/debug');
@@ -439,22 +453,6 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
439
453
  const form = document.getElementById('onekite-settings-form');
440
454
  if (!form) return;
441
455
 
442
- // Inject missing settings fields if the template is older
443
- function ensureTextInput(name, label, help) {
444
- if (form.querySelector(`[name="${name}"]`)) return;
445
- const div = document.createElement('div');
446
- div.className = 'mb-3';
447
- div.innerHTML = `
448
- <label class="form-label">${label}</label>
449
- <input type="text" class="form-control" name="${name}" placeholder="" />
450
- ${help ? `<div class="form-text">${help}</div>` : ''}
451
- `;
452
- form.appendChild(div);
453
- }
454
-
455
- ensureTextInput('specialCreatorGroups', 'Groupes autorisés à créer des évènements (csv)', 'Ex: groupA,groupB');
456
- ensureTextInput('specialDeleterGroups', 'Groupes autorisés à supprimer des évènements (csv)', 'Par défaut, si vide : même liste que la création');
457
-
458
456
  // Load settings
459
457
  try {
460
458
  const s = await loadSettings();
@@ -603,6 +601,28 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
603
601
  });
604
602
  }
605
603
 
604
+ // Purge special events by year
605
+ const sePurgeBtn = document.getElementById('onekite-se-purge');
606
+ if (sePurgeBtn) {
607
+ sePurgeBtn.addEventListener('click', async () => {
608
+ const yearInput = document.getElementById('onekite-se-purge-year');
609
+ const year = (yearInput ? yearInput.value : '').trim();
610
+ if (!/^\d{4}$/.test(year)) {
611
+ showAlert('error', 'Année invalide (YYYY)');
612
+ return;
613
+ }
614
+ bootbox.confirm(`Purger tous les évènements de ${year} ?`, async (ok) => {
615
+ if (!ok) return;
616
+ try {
617
+ const r = await purgeSpecialEvents(year);
618
+ showAlert('success', `Purge OK (${r.removed || 0} supprimé(s)).`);
619
+ } catch (e) {
620
+ showAlert('error', 'Purge impossible.');
621
+ }
622
+ });
623
+ });
624
+ }
625
+
606
626
  // Debug
607
627
  const debugBtn = document.getElementById('onekite-debug-run');
608
628
  if (debugBtn) {
package/public/client.js CHANGED
@@ -118,6 +118,61 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
118
118
  });
119
119
  }
120
120
 
121
+ async function openMapViewer(title, address, lat, lon) {
122
+ const mapId = `onekite-map-view-${Date.now()}-${Math.floor(Math.random()*10000)}`;
123
+ const safeAddr = (address || '').trim();
124
+ const html = `
125
+ ${safeAddr ? `<div class="mb-2">${escapeHtml(safeAddr)}</div>` : ''}
126
+ <div id="${mapId}" style="height:260px; border:1px solid #ddd; border-radius:6px;"></div>
127
+ `;
128
+ bootbox.dialog({
129
+ title: title || 'Carte',
130
+ message: html,
131
+ buttons: { close: { label: 'Fermer', className: 'btn-secondary' } },
132
+ });
133
+ setTimeout(async () => {
134
+ try {
135
+ const el = document.getElementById(mapId);
136
+ if (!el) return;
137
+ const L = await loadLeaflet();
138
+ const map = L.map(el);
139
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '&copy; OpenStreetMap' }).addTo(map);
140
+
141
+ async function setAt(lat2, lon2) {
142
+ map.setView([lat2, lon2], 14);
143
+ L.marker([lat2, lon2]).addTo(map);
144
+ }
145
+
146
+ const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
147
+ if (hasCoords) {
148
+ await setAt(lat, lon);
149
+ return;
150
+ }
151
+ if (safeAddr) {
152
+ const hit = await geocodeAddress(safeAddr);
153
+ if (hit && hit.lat && hit.lon) {
154
+ await setAt(hit.lat, hit.lon);
155
+ return;
156
+ }
157
+ }
158
+ map.setView([46.5, 2.5], 5);
159
+ } catch (e) {
160
+ // ignore leaflet errors
161
+ }
162
+ }, 0);
163
+ }
164
+
165
+ // Click handler for map links in popups
166
+ document.addEventListener('click', (ev) => {
167
+ const a = ev.target && ev.target.closest ? ev.target.closest('a.onekite-map-link') : null;
168
+ if (!a) return;
169
+ ev.preventDefault();
170
+ const addr = a.getAttribute('data-address') || a.textContent || '';
171
+ const lat = parseFloat(a.getAttribute('data-lat'));
172
+ const lon = parseFloat(a.getAttribute('data-lon'));
173
+ openMapViewer('Adresse', addr, lat, lon);
174
+ });
175
+
121
176
  function statusLabel(s) {
122
177
  const map = {
123
178
  pending: 'En attente',
@@ -524,12 +579,20 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
524
579
  ? `<div class="mb-2"><strong>Créé par</strong><br><a href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
525
580
  : '';
526
581
  const addr = String(p.pickupAddress || '').trim();
582
+ const lat = Number(p.pickupLat);
583
+ const lon = Number(p.pickupLon);
584
+ const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
527
585
  const notes = String(p.notes || '').trim();
586
+ const addrHtml = addr
587
+ ? (hasCoords
588
+ ? `<a href="#" class="onekite-map-link" data-address="${escapeHtml(addr)}" data-lat="${escapeHtml(String(lat))}" data-lon="${escapeHtml(String(lon))}">${escapeHtml(addr)}</a>`
589
+ : `${escapeHtml(addr)}`)
590
+ : '';
528
591
  const html = `
529
592
  <div class="mb-2"><strong>Titre</strong><br>${escapeHtml(p.title || ev.title || '')}</div>
530
593
  ${userLine}
531
594
  <div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatDt(ev.start))} → ${escapeHtml(formatDt(ev.end))}</div>
532
- ${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${escapeHtml(addr)}</div>` : ''}
595
+ ${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${addrHtml}</div>` : ''}
533
596
  ${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
534
597
  `;
535
598
  const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
@@ -6,7 +6,10 @@
6
6
 
7
7
  <ul class="nav nav-tabs mt-3" role="tablist">
8
8
  <li class="nav-item" role="presentation">
9
- <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#onekite-tab-settings" type="button" role="tab">Paramètres</button>
9
+ <button class="nav-link active" data-bs-toggle="tab" data-bs-target="#onekite-tab-settings" type="button" role="tab">Locations</button>
10
+ </li>
11
+ <li class="nav-item" role="presentation">
12
+ <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-events" type="button" role="tab">Évènements</button>
10
13
  </li>
11
14
  <li class="nav-item" role="presentation">
12
15
  <button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-pending" type="button" role="tab">Demandes en attente</button>
@@ -20,13 +23,13 @@
20
23
  </ul>
21
24
 
22
25
  <div class="tab-content pt-3">
26
+ <form id="onekite-settings-form" class="mt-1">
23
27
  <div class="tab-pane fade show active" id="onekite-tab-settings" role="tabpanel">
24
- <form id="onekite-settings-form" class="mt-1">
25
28
  <h4>Groupes</h4>
26
29
  <div class="mb-3">
27
30
  <label class="form-label">Groupes autorisés à créer une demande (csv)</label>
28
31
  <input class="form-control" name="creatorGroups" placeholder="ex: registered-users,membres">
29
- <div class="form-text">Si vide, tous les utilisateurs connectés peuvent créer une demande.</div>
32
+ <div class="form-text">Le groupe <code>onekite-ffvl-YYYY</code> est automatiquement ajouté au début. Tu peux ajouter d'autres groupes à la suite.</div>
30
33
  </div>
31
34
 
32
35
  <div class="mb-3">
@@ -84,10 +87,33 @@
84
87
  <label class="form-label">Form Slug</label>
85
88
  <input class="form-control" name="helloassoFormSlug">
86
89
  </div>
90
+ </div>
91
+
92
+ <div class="tab-pane fade" id="onekite-tab-events" role="tabpanel">
93
+ <h4>Évènements (autre couleur)</h4>
94
+ <div class="form-text mb-3">Permet de créer des évènements horaires (début/fin) avec adresse (Leaflet) et notes.</div>
95
+
96
+ <div class="mb-3">
97
+ <label class="form-label">Groupes autorisés à créer des évènements (csv)</label>
98
+ <input class="form-control" name="specialCreatorGroups" placeholder="ex: staff,instructors">
99
+ </div>
87
100
 
88
- </form>
101
+ <div class="mb-3">
102
+ <label class="form-label">Groupes autorisés à supprimer des évènements (csv)</label>
103
+ <input class="form-control" name="specialDeleterGroups" placeholder="Si vide: même liste que la création">
104
+ </div>
105
+
106
+ <hr class="my-4" />
107
+ <h4>Purge des évènements</h4>
108
+ <div class="d-flex gap-2 align-items-center">
109
+ <input class="form-control" style="max-width: 160px;" id="onekite-se-purge-year" placeholder="YYYY">
110
+ <button type="button" class="btn btn-outline-danger" id="onekite-se-purge">Purger</button>
111
+ </div>
112
+ <div class="form-text mt-2">Supprime définitivement tous les évènements dont la date de début est dans l'année sélectionnée.</div>
89
113
  </div>
90
114
 
115
+ </form>
116
+
91
117
  <div class="tab-pane fade" id="onekite-tab-pending" role="tabpanel">
92
118
  <h4>Demandes en attente</h4>
93
119
  <div id="onekite-pending" class="list-group"></div>