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 +1 -0
- package/lib/api.js +17 -0
- package/lib/discord.js +31 -4
- package/lib/scheduler.js +24 -0
- package/lib/widgets.js +12 -10
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/templates/admin/plugins/calendar-onekite.tpl +9 -0
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'
|
|
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'
|
|
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 === '
|
|
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
|
-
:
|
|
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
|
-
|
|
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
package/plugin.json
CHANGED
|
@@ -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">
|