nodebb-plugin-onekite-calendar 1.0.13 → 1.0.15

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/CHANGELOG.md CHANGED
@@ -3,6 +3,7 @@
3
3
  ## 1.0.3
4
4
  - Suppression du texte d’archivage dans le toast de purge (plus de « 0 archivés »)
5
5
  - Renommage du plugin : nodebb-plugin-onekite-calendar
6
+ - Discord : notification « ❌ Réservation annulée » (annulation manuelle + annulation automatique) + option ACP
6
7
 
7
8
  ## 1.0.2
8
9
  - Purge calendrier : suppression réelle des réservations (aucune logique d’archivage)
package/lib/api.js CHANGED
@@ -935,6 +935,23 @@ api.cancelReservation = async function (req, res) {
935
935
  r.cancelledBy = uid;
936
936
 
937
937
  await dbLayer.saveReservation(r);
938
+
939
+ // Discord webhook (optional)
940
+ try {
941
+ await discord.notifyReservationCancelled(settings, {
942
+ rid: r.rid,
943
+ uid: r.uid,
944
+ username: r.username || '',
945
+ itemIds: r.itemIds || [],
946
+ itemNames: r.itemNames || [],
947
+ start: r.start,
948
+ end: r.end,
949
+ status: r.status,
950
+ cancelledAt: r.cancelledAt,
951
+ cancelledBy: r.cancelledBy,
952
+ });
953
+ } catch (e) {}
954
+
938
955
  return res.json({ ok: true, status: 'cancelled' });
939
956
  };
940
957
 
package/lib/discord.js CHANGED
@@ -88,7 +88,9 @@ function buildReservationMessage(kind, reservation) {
88
88
  function buildWebhookPayload(kind, reservation) {
89
89
  // Discord "regroupe" visuellement les messages consécutifs d'un même auteur.
90
90
  // En utilisant un username différent par action, on obtient un message bien distinct.
91
- const webhookUsername = kind === 'paid' ? 'Onekite • Paiement' : 'Onekite • Réservation';
91
+ const webhookUsername = kind === 'paid'
92
+ ? 'Onekite • Paiement'
93
+ : (kind === 'cancelled' ? 'Onekite • Annulation' : 'Onekite • Réservation');
92
94
 
93
95
  const calUrl = 'https://www.onekite.com/calendar';
94
96
  const username = reservation && reservation.username ? String(reservation.username) : '';
@@ -98,20 +100,29 @@ function buildWebhookPayload(kind, reservation) {
98
100
  const start = reservation && reservation.start ? Number(reservation.start) : NaN;
99
101
  const end = reservation && reservation.end ? Number(reservation.end) : NaN;
100
102
 
101
- const title = kind === 'paid' ? '💳 Paiement reçu' : '⏳ Demande de réservation';
103
+ const title = kind === 'paid'
104
+ ? '💳 Paiement reçu'
105
+ : (kind === 'cancelled' ? '❌ Réservation annulée' : '⏳ Demande de réservation');
102
106
 
103
107
  const fields = [];
104
108
  if (username) {
105
109
  fields.push({ name: 'Membre', value: username, inline: true });
106
110
  }
107
111
  if (items.length) {
108
- const label = kind === 'paid' ? 'Matériel' : 'Matériel demandé';
112
+ const label = kind === 'request' ? 'Matériel demandé' : 'Matériel';
109
113
  fields.push({ name: label, value: items.map((it) => `• ${it}`).join('\n'), inline: false });
110
114
  }
111
115
  if (Number.isFinite(start) && Number.isFinite(end)) {
112
116
  fields.push({ name: 'Période', value: `Du ${formatFRShort(start)} au ${formatFRShort(end)}`, inline: false });
113
117
  }
114
118
 
119
+ if (kind === 'cancelled') {
120
+ const cancelledAt = reservation && reservation.cancelledAt ? Number(reservation.cancelledAt) : NaN;
121
+ if (Number.isFinite(cancelledAt)) {
122
+ fields.push({ name: 'Annulée le', value: formatFRShort(cancelledAt), inline: true });
123
+ }
124
+ }
125
+
115
126
  return {
116
127
  username: webhookUsername,
117
128
  // On laisse "content" vide pour privilégier l'embed (plus lisible sur Discord)
@@ -122,7 +133,9 @@ function buildWebhookPayload(kind, reservation) {
122
133
  url: calUrl,
123
134
  description: kind === 'paid'
124
135
  ? 'Un paiement a été reçu pour une réservation.'
125
- : 'Une nouvelle demande de réservation a été créée.',
136
+ : (kind === 'cancelled'
137
+ ? 'Une réservation a été annulée.'
138
+ : 'Une nouvelle demande de réservation a été créée.'),
126
139
  fields,
127
140
  footer: { text: 'Onekite • Calendrier' },
128
141
  timestamp: new Date().toISOString(),
@@ -157,7 +170,21 @@ async function notifyPaymentReceived(settings, reservation) {
157
170
  }
158
171
  }
159
172
 
173
+ async function notifyReservationCancelled(settings, reservation) {
174
+ const url = settings && settings.discordWebhookUrl ? String(settings.discordWebhookUrl).trim() : '';
175
+ if (!url) return;
176
+ if (!isEnabled(settings.discordNotifyOnCancelled, true)) return;
177
+
178
+ try {
179
+ await postWebhook(url, buildWebhookPayload('cancelled', reservation));
180
+ } catch (e) {
181
+ // eslint-disable-next-line no-console
182
+ console.warn('[calendar-onekite] Discord webhook failed (cancelled)', e && e.message ? e.message : String(e));
183
+ }
184
+ }
185
+
160
186
  module.exports = {
161
187
  notifyReservationRequested,
162
188
  notifyPaymentReceived,
189
+ notifyReservationCancelled,
163
190
  };
package/lib/scheduler.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const meta = require.main.require('./src/meta');
4
4
  const db = require.main.require('./src/database');
5
5
  const dbLayer = require('./db');
6
+ const discord = require('./discord');
6
7
 
7
8
  let timer = null;
8
9
 
@@ -144,6 +145,12 @@ async function processAwaitingPayment() {
144
145
  const expiredKey = 'calendar-onekite:email:expiredSent';
145
146
  const firstExpired = await db.setAdd(expiredKey, rid);
146
147
  const shouldEmail = !!firstExpired;
148
+
149
+ // Guard Discord notification across clustered NodeBB processes
150
+ const discordKey = 'calendar-onekite:discord:cancelledSent';
151
+ const firstDiscord = await db.setAdd(discordKey, rid);
152
+ const shouldDiscord = !!firstDiscord;
153
+
147
154
  const u = await user.getUserFields(r.uid, ['username', 'email']);
148
155
  if (shouldEmail && u && u.email) {
149
156
  await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
@@ -154,6 +161,23 @@ async function processAwaitingPayment() {
154
161
  delayMinutes: holdMins,
155
162
  });
156
163
  }
164
+
165
+ if (shouldDiscord) {
166
+ try {
167
+ await discord.notifyReservationCancelled(settings, {
168
+ rid: r.rid || rid,
169
+ uid: r.uid,
170
+ username: (u && u.username) ? u.username : (r.username || ''),
171
+ itemIds: r.itemIds || [],
172
+ itemNames: r.itemNames || [],
173
+ start: r.start,
174
+ end: r.end,
175
+ status: 'cancelled',
176
+ cancelledAt: now,
177
+ cancelledBy: 'system',
178
+ });
179
+ } catch (e) {}
180
+ }
157
181
  await dbLayer.removeReservation(rid);
158
182
  }
159
183
  }
package/lib/widgets.js CHANGED
@@ -169,9 +169,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
169
169
  height: 'auto',
170
170
  headerToolbar: {
171
171
  left: 'prev,next',
172
- // No range/title in the widget: it becomes redundant and, when the
173
- // view crosses months, FullCalendar displays a start/end range.
174
- center: '',
172
+ center: 'title',
175
173
  right: '',
176
174
  },
177
175
  navLinks: false,
@@ -188,6 +186,16 @@ widgets.renderTwoWeeksWidget = async function (data) {
188
186
  return { html: '<span class="fc-daygrid-day-number">' + String(arg.dayNumberText || '') + '</span>' };
189
187
  }
190
188
  },
189
+ // Ensure day numbers never include month text (e.g. '1 janvier')
190
+ dayCellDidMount: function(arg) {
191
+ try {
192
+ const el = arg && arg.el ? arg.el.querySelector('.fc-daygrid-day-number') : null;
193
+ if (!el) return;
194
+ const t = String(el.textContent || '');
195
+ const m = t.match(/^\s*(\d+)/);
196
+ if (m) el.textContent = m[1];
197
+ } catch (e) {}
198
+ },
191
199
  eventClassNames: function(arg) {
192
200
  try {
193
201
  const ev = arg && arg.event;
@@ -281,14 +289,8 @@ dateClick: function() { window.location.href = calUrl; },
281
289
  return map[k] || '';
282
290
  });
283
291
  const status = (String(ep.type || '') === 'reservation') ? statusLabel(ep.status) : '';
284
- const start = ev.start ? new Date(ev.start) : null;
285
- const end = ev.end ? new Date(ev.end) : null;
286
- const pad2 = (n) => String(n).padStart(2, '0');
287
- const fmt = (d) => d ? (pad2(d.getDate()) + '/' + pad2(d.getMonth() + 1) + '/' + String(d.getFullYear()).slice(-2)) : '';
288
- const range = (start && end) ? ('Du ' + fmt(start) + ' au ' + fmt(end)) : '';
289
292
  const html = '' +
290
293
  '<div style="font-weight:600; margin-bottom:2px;">' + escapeHtml(title) + '</div>' +
291
- (range ? ('<div style="opacity:.85">' + escapeHtml(range) + '</div>') : '') +
292
294
  (status ? ('<div style="opacity:.75; margin-top:2px; font-size:.85em;">' + escapeHtml(status) + '</div>') : '');
293
295
 
294
296
  // Hover (desktop)
@@ -422,4 +424,4 @@ dateClick: function() { window.location.href = calUrl; },
422
424
  return data;
423
425
  };
424
426
 
425
- module.exports = widgets;
427
+ module.exports = widgets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
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/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "1.0.13"
42
+ "version": "1.0.15"
43
43
  }
@@ -78,6 +78,15 @@
78
78
  </select>
79
79
  </div>
80
80
 
81
+ <div class="mb-3">
82
+ <label class="form-label">Envoyer une notification à l'annulation</label>
83
+ <select class="form-select" name="discordNotifyOnCancelled">
84
+ <option value="1">Oui</option>
85
+ <option value="0">Non</option>
86
+ </select>
87
+ <div class="form-text">Inclut les annulations manuelles et les annulations automatiques (paiement non reçu dans les délais).</div>
88
+ </div>
89
+
81
90
  <h4 class="mt-4">HelloAsso</h4>
82
91
 
83
92
  <div class="mb-3">