nodebb-plugin-calendar-onekite 11.1.85 → 11.1.86
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/api.js +25 -20
- package/lib/scheduler.js +22 -72
- package/library.js +19 -104
- package/package.json +2 -2
- package/plugin.json +5 -11
- package/public/client.js +22 -124
- package/templates/admin/plugins/calendar-onekite.tpl +11 -9
package/lib/api.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
|
|
5
5
|
const meta = require.main.require('./src/meta');
|
|
6
|
+
const emailer = require.main.require('./src/emailer');
|
|
6
7
|
const nconf = require.main.require('nconf');
|
|
7
8
|
const user = require.main.require('./src/user');
|
|
8
9
|
const groups = require.main.require('./src/groups');
|
|
@@ -14,35 +15,33 @@ const helloasso = require('./helloasso');
|
|
|
14
15
|
// We try the common forms. Any failure is logged for debugging.
|
|
15
16
|
async function sendEmail(template, toEmail, subject, data) {
|
|
16
17
|
if (!toEmail) return;
|
|
17
|
-
const emailer = require.main.require('./src/emailer');
|
|
18
|
-
const dataWithSubject = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
19
18
|
try {
|
|
20
|
-
// NodeBB
|
|
19
|
+
// NodeBB core signature (historically):
|
|
20
|
+
// Emailer.sendToEmail(template, email, language, params[, callback])
|
|
21
|
+
// Subject is not a positional arg; it must be injected (either by NodeBB itself
|
|
22
|
+
// or via filter:email.modify). We always pass it in params.subject.
|
|
23
|
+
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
24
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
21
25
|
if (typeof emailer.sendToEmail === 'function') {
|
|
22
|
-
|
|
23
|
-
if (emailer.sendToEmail.length >= 3) {
|
|
24
|
-
await emailer.sendToEmail(template, toEmail, dataWithSubject);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
// Fallback
|
|
28
|
-
await emailer.sendToEmail(template, toEmail, subject, data);
|
|
26
|
+
await emailer.sendToEmail(template, toEmail, language, params);
|
|
29
27
|
return;
|
|
30
28
|
}
|
|
29
|
+
// Fallback for older/unusual builds (rare)
|
|
31
30
|
if (typeof emailer.send === 'function') {
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// Some builds accept (template, email, language, params)
|
|
32
|
+
if (emailer.send.length >= 4) {
|
|
33
|
+
await emailer.send(template, toEmail, language, params);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Some builds accept (template, email, params)
|
|
37
|
+
await emailer.send(template, toEmail, params);
|
|
38
38
|
}
|
|
39
39
|
} catch (err) {
|
|
40
40
|
// eslint-disable-next-line no-console
|
|
41
|
-
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(
|
|
41
|
+
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
45
|
function normalizeBaseUrl(meta) {
|
|
47
46
|
// Prefer meta.config.url, fallback to nconf.get('url')
|
|
48
47
|
let base = (meta && meta.config && (meta.config.url || meta.config['url'])) ? (meta.config.url || meta.config['url']) : '';
|
|
@@ -184,6 +183,7 @@ async function canDeleteSpecial(uid, settings) {
|
|
|
184
183
|
function eventsFor(resv) {
|
|
185
184
|
const status = resv.status;
|
|
186
185
|
const icons = { pending: '⏳', awaiting_payment: '💳', paid: '✅' };
|
|
186
|
+
const colors = { pending: '#f39c12', awaiting_payment: '#d35400', paid: '#27ae60' };
|
|
187
187
|
const startIsoDate = new Date(parseInt(resv.start, 10)).toISOString().slice(0, 10);
|
|
188
188
|
const endIsoDate = new Date(parseInt(resv.end, 10)).toISOString().slice(0, 10);
|
|
189
189
|
|
|
@@ -200,6 +200,9 @@ function eventsFor(resv) {
|
|
|
200
200
|
// keep id unique per item for FullCalendar, but keep the real rid in extendedProps.rid
|
|
201
201
|
id: `${resv.rid}:${itemId || i}`,
|
|
202
202
|
title: `${icons[status] || ''} ${itemName}`.trim(),
|
|
203
|
+
backgroundColor: colors[status] || '#3498db',
|
|
204
|
+
borderColor: colors[status] || '#3498db',
|
|
205
|
+
textColor: '#ffffff',
|
|
203
206
|
allDay: true,
|
|
204
207
|
start: startIsoDate,
|
|
205
208
|
end: endIsoDate,
|
|
@@ -228,7 +231,9 @@ function eventsForSpecial(ev) {
|
|
|
228
231
|
allDay: false,
|
|
229
232
|
start: startIso,
|
|
230
233
|
end: endIso,
|
|
231
|
-
|
|
234
|
+
backgroundColor: '#8e44ad',
|
|
235
|
+
borderColor: '#8e44ad',
|
|
236
|
+
textColor: '#ffffff',
|
|
232
237
|
extendedProps: {
|
|
233
238
|
type: 'special',
|
|
234
239
|
eid: ev.eid,
|
|
@@ -496,7 +501,7 @@ api.createReservation = async function (req, res) {
|
|
|
496
501
|
await sendEmail(
|
|
497
502
|
'calendar-onekite_pending',
|
|
498
503
|
md.email,
|
|
499
|
-
'Location - Demande de réservation',
|
|
504
|
+
'Location matériel - Demande de réservation',
|
|
500
505
|
{
|
|
501
506
|
username: md.username,
|
|
502
507
|
requester: requester.username,
|
|
@@ -594,7 +599,7 @@ api.approveReservation = async function (req, res) {
|
|
|
594
599
|
const mapUrl = (Number.isFinite(latNum) && Number.isFinite(lonNum))
|
|
595
600
|
? `https://www.openstreetmap.org/?mlat=${encodeURIComponent(String(latNum))}&mlon=${encodeURIComponent(String(lonNum))}#map=18/${encodeURIComponent(String(latNum))}/${encodeURIComponent(String(lonNum))}`
|
|
596
601
|
: '';
|
|
597
|
-
await sendEmail('calendar-onekite_approved', requester.email, 'Location - Réservation validée', {
|
|
602
|
+
await sendEmail('calendar-onekite_approved', requester.email, 'Location matériel - Réservation validée', {
|
|
598
603
|
username: requester.username,
|
|
599
604
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
600
605
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
@@ -630,7 +635,7 @@ api.refuseReservation = async function (req, res) {
|
|
|
630
635
|
|
|
631
636
|
const requester = await user.getUserFields(r.uid, ['username', 'email']);
|
|
632
637
|
if (requester && requester.email) {
|
|
633
|
-
await sendEmail('calendar-onekite_refused', requester.email, 'Location - Demande de réservation', {
|
|
638
|
+
await sendEmail('calendar-onekite_refused', requester.email, 'Location matériel - Demande de réservation', {
|
|
634
639
|
username: requester.username,
|
|
635
640
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
636
641
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
package/lib/scheduler.js
CHANGED
|
@@ -56,81 +56,31 @@ async function processAwaitingPayment() {
|
|
|
56
56
|
const user = require.main.require('./src/user');
|
|
57
57
|
|
|
58
58
|
async function sendEmail(template, toEmail, subject, data) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (emailer.send.length === 3) {
|
|
71
|
-
await emailer.send(template, toEmail, data);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
await emailer.send(template, toEmail, subject, data);
|
|
75
|
-
}
|
|
76
|
-
} catch (err) {
|
|
77
|
-
// eslint-disable-next-line no-console
|
|
78
|
-
console.warn('[calendar-onekite] Failed to send email (scheduler)', { template, toEmail, err: String(err && err.message || err) });
|
|
59
|
+
if (!toEmail) return;
|
|
60
|
+
try {
|
|
61
|
+
// NodeBB core signature (historically):
|
|
62
|
+
// Emailer.sendToEmail(template, email, language, params[, callback])
|
|
63
|
+
// Subject is not a positional arg; it must be injected (either by NodeBB itself
|
|
64
|
+
// or via filter:email.modify). We always pass it in params.subject.
|
|
65
|
+
const language = (meta && meta.config && (meta.config.defaultLang || meta.config.defaultLanguage)) || 'fr';
|
|
66
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
67
|
+
if (typeof emailer.sendToEmail === 'function') {
|
|
68
|
+
await emailer.sendToEmail(template, toEmail, language, params);
|
|
69
|
+
return;
|
|
79
70
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const yyyy = d.getFullYear();
|
|
87
|
-
return `${dd}/${mm}/${yyyy}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
for (const rid of ids) {
|
|
91
|
-
const r = await dbLayer.getReservation(rid);
|
|
92
|
-
if (!r || r.status !== 'awaiting_payment') continue;
|
|
93
|
-
|
|
94
|
-
const approvedAt = parseInt(r.approvedAt || r.validatedAt || 0, 10) || 0;
|
|
95
|
-
if (!approvedAt) continue;
|
|
96
|
-
|
|
97
|
-
const reminderAt = approvedAt + holdMins * 60 * 1000;
|
|
98
|
-
const expireAt = approvedAt + 2 * holdMins * 60 * 1000;
|
|
99
|
-
|
|
100
|
-
if (!r.reminderSent && now >= reminderAt && now < expireAt) {
|
|
101
|
-
// Send reminder once
|
|
102
|
-
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
103
|
-
if (u && u.email) {
|
|
104
|
-
await sendEmail('calendar-onekite_reminder', u.email, 'Location matériel - Rappel', {
|
|
105
|
-
username: u.username,
|
|
106
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
107
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
108
|
-
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
109
|
-
paymentUrl: r.paymentUrl || '',
|
|
110
|
-
delayMinutes: holdMins,
|
|
111
|
-
pickupLine: r.pickupTime ? (r.adminNote ? `${r.pickupTime} à ${r.adminNote}` : r.pickupTime) : '',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
r.reminderSent = true;
|
|
115
|
-
r.reminderAt = now;
|
|
116
|
-
await dbLayer.saveReservation(r);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (now >= expireAt) {
|
|
121
|
-
// Expire: remove reservation so it disappears from calendar and frees items
|
|
122
|
-
const u = await user.getUserFields(r.uid, ['username', 'email']);
|
|
123
|
-
if (u && u.email) {
|
|
124
|
-
await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
|
|
125
|
-
username: u.username,
|
|
126
|
-
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
127
|
-
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
128
|
-
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
129
|
-
delayMinutes: holdMins,
|
|
130
|
-
});
|
|
71
|
+
// Fallback for older/unusual builds (rare)
|
|
72
|
+
if (typeof emailer.send === 'function') {
|
|
73
|
+
// Some builds accept (template, email, language, params)
|
|
74
|
+
if (emailer.send.length >= 4) {
|
|
75
|
+
await emailer.send(template, toEmail, language, params);
|
|
76
|
+
return;
|
|
131
77
|
}
|
|
132
|
-
|
|
78
|
+
// Some builds accept (template, email, params)
|
|
79
|
+
await emailer.send(template, toEmail, params);
|
|
133
80
|
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.warn('[calendar-onekite] Failed to send email', { template, toEmail, err: String(err) });
|
|
134
84
|
}
|
|
135
85
|
}
|
|
136
86
|
|
package/library.js
CHANGED
|
@@ -19,110 +19,6 @@ const Plugin = {};
|
|
|
19
19
|
const isFn = (fn) => typeof fn === 'function';
|
|
20
20
|
const mw = (...fns) => fns.filter(isFn);
|
|
21
21
|
|
|
22
|
-
function escapeHtml(str) {
|
|
23
|
-
return String(str)
|
|
24
|
-
.replace(/&/g, '&')
|
|
25
|
-
.replace(/</g, '<')
|
|
26
|
-
.replace(/>/g, '>')
|
|
27
|
-
.replace(/"/g, '"')
|
|
28
|
-
.replace(/'/g, ''');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// --- Widgets ---------------------------------------------------------------
|
|
32
|
-
// Docs: https://docs.nodebb.org/development/widgets/
|
|
33
|
-
Plugin.defineWidgets = async function (widgets, callback) {
|
|
34
|
-
try {
|
|
35
|
-
widgets = widgets || [];
|
|
36
|
-
widgets.push({
|
|
37
|
-
widget: 'calendar-onekite-mini',
|
|
38
|
-
name: 'Calendrier OneKite (mini)',
|
|
39
|
-
description: 'Mini calendrier (mois en cours) avec indicateurs de réservations/évènements.',
|
|
40
|
-
// Widget settings form (ACP Widgets editor)
|
|
41
|
-
content: [
|
|
42
|
-
'<div class="form-group">',
|
|
43
|
-
' <label>Titre</label>',
|
|
44
|
-
' <input type="text" class="form-control" name="title" placeholder="Calendrier" />',
|
|
45
|
-
'</div>',
|
|
46
|
-
'<div class="form-group">',
|
|
47
|
-
' <label>Afficher les évènements</label>',
|
|
48
|
-
' <select class="form-control" name="showSpecial">',
|
|
49
|
-
' <option value="1">Oui</option>',
|
|
50
|
-
' <option value="0">Non</option>',
|
|
51
|
-
' </select>',
|
|
52
|
-
'</div>',
|
|
53
|
-
].join(''),
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (typeof callback === 'function') {
|
|
57
|
-
return callback(null, widgets);
|
|
58
|
-
}
|
|
59
|
-
return widgets;
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (typeof callback === 'function') {
|
|
62
|
-
return callback(err);
|
|
63
|
-
}
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
Plugin.renderMiniWidget = async function (hookData, callback) {
|
|
69
|
-
try {
|
|
70
|
-
const widget = hookData.widget || {};
|
|
71
|
-
const data = widget.data || {};
|
|
72
|
-
const title = (data.title || 'Calendrier').toString();
|
|
73
|
-
const showSpecial = String(data.showSpecial ?? '1') !== '0';
|
|
74
|
-
|
|
75
|
-
const widgetId = `calendar-onekite-mini-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
|
|
76
|
-
|
|
77
|
-
// Note: We render client-side to avoid extra server-side queries. The widget fetches
|
|
78
|
-
// events via the existing public API endpoint.
|
|
79
|
-
widget.html = `
|
|
80
|
-
<div class="calendar-onekite-widget" id="${widgetId}" data-show-special="${showSpecial ? '1' : '0'}">
|
|
81
|
-
<div class="calendar-onekite-widget__header">
|
|
82
|
-
<strong>${escapeHtml(title)}</strong>
|
|
83
|
-
<a class="calendar-onekite-widget__link" href="/calendar">Ouvrir</a>
|
|
84
|
-
</div>
|
|
85
|
-
<div class="calendar-onekite-widget__month"></div>
|
|
86
|
-
<div class="calendar-onekite-widget__legend">
|
|
87
|
-
<span class="calendar-onekite-dot calendar-onekite-dot--pending"></span> en attente
|
|
88
|
-
<span class="calendar-onekite-dot calendar-onekite-dot--paid"></span> payée
|
|
89
|
-
${showSpecial ? '<span class="calendar-onekite-dot calendar-onekite-dot--special"></span> évènement' : ''}
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
<style>
|
|
93
|
-
.calendar-onekite-widget{border:1px solid rgba(0,0,0,.1);border-radius:8px;padding:10px}
|
|
94
|
-
.calendar-onekite-widget__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
|
95
|
-
.calendar-onekite-widget__link{font-size:12px}
|
|
96
|
-
.calendar-onekite-widget__grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
|
|
97
|
-
.calendar-onekite-widget__cell{position:relative;aspect-ratio:1/1;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;user-select:none}
|
|
98
|
-
.calendar-onekite-widget__cell--muted{opacity:.35}
|
|
99
|
-
.calendar-onekite-widget__cell--today{outline:2px solid rgba(0,0,0,.2)}
|
|
100
|
-
.calendar-onekite-badges{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);display:flex;gap:2px}
|
|
101
|
-
.calendar-onekite-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin:0 4px 0 10px;vertical-align:middle}
|
|
102
|
-
.calendar-onekite-dot--pending{background:#0d6efd}
|
|
103
|
-
.calendar-onekite-dot--paid{background:#198754}
|
|
104
|
-
.calendar-onekite-dot--special{background:#fd7e14}
|
|
105
|
-
.calendar-onekite-badge{width:6px;height:6px;border-radius:50%}
|
|
106
|
-
.calendar-onekite-badge--pending{background:#0d6efd}
|
|
107
|
-
.calendar-onekite-badge--paid{background:#198754}
|
|
108
|
-
.calendar-onekite-badge--special{background:#fd7e14}
|
|
109
|
-
.calendar-onekite-widget__legend{margin-top:8px;font-size:12px;opacity:.8}
|
|
110
|
-
</style>
|
|
111
|
-
`;
|
|
112
|
-
|
|
113
|
-
hookData.widget = widget;
|
|
114
|
-
if (typeof callback === 'function') {
|
|
115
|
-
return callback(null, hookData);
|
|
116
|
-
}
|
|
117
|
-
return hookData;
|
|
118
|
-
} catch (err) {
|
|
119
|
-
if (typeof callback === 'function') {
|
|
120
|
-
return callback(err);
|
|
121
|
-
}
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
22
|
Plugin.init = async function (params) {
|
|
127
23
|
const { router, middleware } = params;
|
|
128
24
|
|
|
@@ -252,4 +148,23 @@ Plugin.addAdminNavigation = async function (header) {
|
|
|
252
148
|
return header;
|
|
253
149
|
};
|
|
254
150
|
|
|
151
|
+
|
|
152
|
+
// Ensure our transactional emails always get a subject.
|
|
153
|
+
// NodeBB's Emailer.sendToEmail signature expects (template, email, language, params),
|
|
154
|
+
// so plugins typically inject/modify the subject via this hook.
|
|
155
|
+
Plugin.emailModify = async function (data) {
|
|
156
|
+
try {
|
|
157
|
+
if (!data || !data.template) return data;
|
|
158
|
+
const tpl = String(data.template);
|
|
159
|
+
if (!tpl.startsWith('calendar-onekite_')) return data;
|
|
160
|
+
|
|
161
|
+
// If the caller provided a subject (we pass it in params.subject), copy it to data.subject.
|
|
162
|
+
const provided = data.params && data.params.subject ? String(data.params.subject) : '';
|
|
163
|
+
if (provided && (!data.subject || !String(data.subject).trim())) {
|
|
164
|
+
data.subject = provided;
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {}
|
|
167
|
+
return data;
|
|
168
|
+
};
|
|
169
|
+
|
|
255
170
|
module.exports = Plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-calendar-onekite",
|
|
3
|
-
"version": "11.1.
|
|
3
|
+
"version": "11.1.86",
|
|
4
4
|
"description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
"node": ">=18"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {}
|
|
11
|
-
}
|
|
11
|
+
}
|
package/plugin.json
CHANGED
|
@@ -8,19 +8,13 @@
|
|
|
8
8
|
"hook": "static:app.load",
|
|
9
9
|
"method": "init"
|
|
10
10
|
},
|
|
11
|
-
{
|
|
12
|
-
"hook": "filter:widgets.getWidgets",
|
|
13
|
-
"method": "defineWidgets",
|
|
14
|
-
"callbacked": true
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"hook": "filter:widget.render:calendar-onekite-mini",
|
|
18
|
-
"method": "renderMiniWidget",
|
|
19
|
-
"callbacked": true
|
|
20
|
-
},
|
|
21
11
|
{
|
|
22
12
|
"hook": "filter:admin.header.build",
|
|
23
13
|
"method": "addAdminNavigation"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"hook": "filter:email.modify",
|
|
17
|
+
"method": "emailModify"
|
|
24
18
|
}
|
|
25
19
|
],
|
|
26
20
|
"staticDirs": {
|
|
@@ -37,5 +31,5 @@
|
|
|
37
31
|
"acpScripts": [
|
|
38
32
|
"public/admin.js"
|
|
39
33
|
],
|
|
40
|
-
"version": "1.0.
|
|
34
|
+
"version": "1.0.47"
|
|
41
35
|
}
|
package/public/client.js
CHANGED
|
@@ -18,15 +18,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
18
18
|
|
|
19
19
|
async function openSpecialEventDialog(selectionInfo) {
|
|
20
20
|
const start = selectionInfo.start;
|
|
21
|
-
|
|
22
|
-
// FullCalendar all-day selection uses an exclusive end (next day 00:00). For a single-day click,
|
|
23
|
-
// this looks like a 2-day range. Default to a 1-hour duration in that case.
|
|
24
|
-
try {
|
|
25
|
-
if (selectionInfo && selectionInfo.allDay && start && end && (end.getTime() - start.getTime()) <= 86400000 + 1000) {
|
|
26
|
-
end = new Date(start.getTime() + 60 * 60 * 1000);
|
|
27
|
-
}
|
|
28
|
-
} catch (e) {}
|
|
29
|
-
|
|
21
|
+
const end = selectionInfo.end;
|
|
30
22
|
const html = `
|
|
31
23
|
<div class="mb-3">
|
|
32
24
|
<label class="form-label">Titre</label>
|
|
@@ -59,7 +51,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
59
51
|
`;
|
|
60
52
|
|
|
61
53
|
return await new Promise((resolve) => {
|
|
62
|
-
|
|
54
|
+
bootbox.dialog({
|
|
63
55
|
title: 'Créer un évènement',
|
|
64
56
|
message: html,
|
|
65
57
|
buttons: {
|
|
@@ -86,18 +78,14 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
86
78
|
},
|
|
87
79
|
});
|
|
88
80
|
|
|
89
|
-
// init leaflet
|
|
90
|
-
|
|
81
|
+
// init leaflet
|
|
82
|
+
setTimeout(async () => {
|
|
91
83
|
try {
|
|
92
84
|
const mapEl = document.getElementById('onekite-se-map');
|
|
93
85
|
if (!mapEl) return;
|
|
94
86
|
const L = await loadLeaflet();
|
|
95
87
|
const map = L.map(mapEl).setView([46.5, 2.5], 5);
|
|
96
88
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map);
|
|
97
|
-
|
|
98
|
-
setTimeout(() => {
|
|
99
|
-
try { map.invalidateSize(); } catch (e) {}
|
|
100
|
-
}, 50);
|
|
101
89
|
let marker = null;
|
|
102
90
|
function setMarker(lat, lon) {
|
|
103
91
|
if (marker) map.removeLayer(marker);
|
|
@@ -126,7 +114,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
126
114
|
} catch (e) {
|
|
127
115
|
// ignore leaflet errors
|
|
128
116
|
}
|
|
129
|
-
});
|
|
117
|
+
}, 0);
|
|
130
118
|
});
|
|
131
119
|
}
|
|
132
120
|
|
|
@@ -137,12 +125,12 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
137
125
|
${safeAddr ? `<div class="mb-2">${escapeHtml(safeAddr)}</div>` : ''}
|
|
138
126
|
<div id="${mapId}" style="height:260px; border:1px solid #ddd; border-radius:6px;"></div>
|
|
139
127
|
`;
|
|
140
|
-
|
|
128
|
+
bootbox.dialog({
|
|
141
129
|
title: title || 'Carte',
|
|
142
130
|
message: html,
|
|
143
131
|
buttons: { close: { label: 'Fermer', className: 'btn-secondary' } },
|
|
144
132
|
});
|
|
145
|
-
|
|
133
|
+
setTimeout(async () => {
|
|
146
134
|
try {
|
|
147
135
|
const el = document.getElementById(mapId);
|
|
148
136
|
if (!el) return;
|
|
@@ -168,14 +156,10 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
168
156
|
}
|
|
169
157
|
}
|
|
170
158
|
map.setView([46.5, 2.5], 5);
|
|
171
|
-
|
|
172
|
-
setTimeout(() => {
|
|
173
|
-
try { map.invalidateSize(); } catch (e) {}
|
|
174
|
-
}, 50);
|
|
175
159
|
} catch (e) {
|
|
176
160
|
// ignore leaflet errors
|
|
177
161
|
}
|
|
178
|
-
});
|
|
162
|
+
}, 0);
|
|
179
163
|
}
|
|
180
164
|
|
|
181
165
|
// Click handler for map links in popups
|
|
@@ -348,6 +332,16 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
348
332
|
}
|
|
349
333
|
}
|
|
350
334
|
|
|
335
|
+
|
|
336
|
+
function formatDtWithTime(d) {
|
|
337
|
+
try {
|
|
338
|
+
return new Date(d).toLocaleString('fr-FR', { dateStyle: 'short', timeStyle: 'short' });
|
|
339
|
+
} catch (e) {
|
|
340
|
+
return String(d);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
351
345
|
function toDatetimeLocalValue(date) {
|
|
352
346
|
const d = new Date(date);
|
|
353
347
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -552,10 +546,9 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
552
546
|
isDialogOpen = false;
|
|
553
547
|
return;
|
|
554
548
|
}
|
|
555
|
-
// Send date
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
const endDate = info.endStr;
|
|
549
|
+
// Send date strings (no hours) so reservations are day-based.
|
|
550
|
+
const startDate = new Date(info.start).toISOString().slice(0, 10);
|
|
551
|
+
const endDate = new Date(info.end).toISOString().slice(0, 10);
|
|
559
552
|
await requestReservation({
|
|
560
553
|
start: startDate,
|
|
561
554
|
end: endDate,
|
|
@@ -608,7 +601,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
608
601
|
const html = `
|
|
609
602
|
<div class="mb-2"><strong>Titre</strong><br>${escapeHtml(p.title || ev.title || '')}</div>
|
|
610
603
|
${userLine}
|
|
611
|
-
<div class="mb-2"><strong>Période</strong><br>${escapeHtml(
|
|
604
|
+
<div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatDtWithTime(ev.start))} → ${escapeHtml(formatDtWithTime(ev.end))}</div>
|
|
612
605
|
${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${addrHtml}</div>` : ''}
|
|
613
606
|
${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
|
|
614
607
|
`;
|
|
@@ -823,12 +816,6 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
823
816
|
// Default view (France-ish)
|
|
824
817
|
map.setView([46.7, 2.5], 5);
|
|
825
818
|
|
|
826
|
-
// Bootbox/Bootstrap modal layouts can report a zero-sized container on first paint.
|
|
827
|
-
// Force Leaflet to recompute sizes once the modal is fully displayed.
|
|
828
|
-
setTimeout(() => {
|
|
829
|
-
try { map.invalidateSize(); } catch (e) {}
|
|
830
|
-
}, 50);
|
|
831
|
-
|
|
832
819
|
let marker = null;
|
|
833
820
|
function setMarker(lat, lon, zoom) {
|
|
834
821
|
const ll = [lat, lon];
|
|
@@ -926,95 +913,6 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
926
913
|
// call once after current tick.
|
|
927
914
|
setTimeout(() => autoInit({ template: (ajaxify && ajaxify.data && ajaxify.data.template) || { name: '' } }), 0);
|
|
928
915
|
|
|
929
|
-
// Render mini widgets (ACP Widgets) without inline scripts (CSP-friendly).
|
|
930
|
-
async function renderMiniWidgets() {
|
|
931
|
-
const widgets = Array.from(document.querySelectorAll('.calendar-onekite-widget'));
|
|
932
|
-
if (!widgets.length) return;
|
|
933
|
-
|
|
934
|
-
const now = new Date();
|
|
935
|
-
const year = now.getFullYear();
|
|
936
|
-
const month = now.getMonth();
|
|
937
|
-
|
|
938
|
-
const start = new Date(year, month, 1);
|
|
939
|
-
start.setDate(start.getDate() - ((start.getDay() + 6) % 7)); // Monday-start
|
|
940
|
-
const end = new Date(start);
|
|
941
|
-
end.setDate(end.getDate() + 41);
|
|
942
|
-
|
|
943
|
-
function iso(d){ return d.toISOString(); }
|
|
944
|
-
function ymd(d){ return d.toISOString().slice(0, 10); }
|
|
945
|
-
|
|
946
|
-
let events = [];
|
|
947
|
-
try {
|
|
948
|
-
const qs = new URLSearchParams({ start: iso(start), end: iso(end) });
|
|
949
|
-
events = await fetchJson(`/api/v3/plugins/calendar-onekite/events?${qs.toString()}`)
|
|
950
|
-
.catch(() => fetchJson(`/api/plugins/calendar-onekite/events?${qs.toString()}`));
|
|
951
|
-
} catch (e) {
|
|
952
|
-
events = [];
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const dayMap = new Map();
|
|
956
|
-
for (const ev of (events || [])) {
|
|
957
|
-
if (!ev || !ev.start) continue;
|
|
958
|
-
const d = new Date(ev.start);
|
|
959
|
-
const key = ymd(d);
|
|
960
|
-
const props = ev.extendedProps || {};
|
|
961
|
-
const isSpecial = props.type === 'special' || ev.type === 'special' || (ev.classNames || []).includes('onekite-special');
|
|
962
|
-
const status = props.status || ev.status || '';
|
|
963
|
-
const bucket = dayMap.get(key) || { pending: 0, paid: 0, special: 0 };
|
|
964
|
-
if (isSpecial) bucket.special++;
|
|
965
|
-
else if (status === 'paid') bucket.paid++;
|
|
966
|
-
else bucket.pending++;
|
|
967
|
-
dayMap.set(key, bucket);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
for (const widgetEl of widgets) {
|
|
971
|
-
const monthEl = widgetEl.querySelector('.calendar-onekite-widget__month');
|
|
972
|
-
if (!monthEl) continue;
|
|
973
|
-
const showSpecial = widgetEl.getAttribute('data-show-special') === '1';
|
|
974
|
-
|
|
975
|
-
const grid = document.createElement('div');
|
|
976
|
-
grid.className = 'calendar-onekite-widget__grid';
|
|
977
|
-
const labels = ['L','M','M','J','V','S','D'];
|
|
978
|
-
for (const l of labels) {
|
|
979
|
-
const el = document.createElement('div');
|
|
980
|
-
el.className = 'calendar-onekite-widget__cell calendar-onekite-widget__cell--muted';
|
|
981
|
-
el.textContent = l;
|
|
982
|
-
grid.appendChild(el);
|
|
983
|
-
}
|
|
984
|
-
for (let i = 0; i < 42; i++) {
|
|
985
|
-
const d = new Date(start);
|
|
986
|
-
d.setDate(start.getDate() + i);
|
|
987
|
-
const cell = document.createElement('a');
|
|
988
|
-
cell.href = '/calendar';
|
|
989
|
-
cell.className = 'calendar-onekite-widget__cell';
|
|
990
|
-
if (d.getMonth() !== month) cell.classList.add('calendar-onekite-widget__cell--muted');
|
|
991
|
-
if (ymd(d) === ymd(now)) cell.classList.add('calendar-onekite-widget__cell--today');
|
|
992
|
-
cell.textContent = d.getDate();
|
|
993
|
-
|
|
994
|
-
const b = dayMap.get(ymd(d));
|
|
995
|
-
if (b && (b.pending || b.paid || (showSpecial && b.special))) {
|
|
996
|
-
const badges = document.createElement('div');
|
|
997
|
-
badges.className = 'calendar-onekite-badges';
|
|
998
|
-
if (b.pending) { const s = document.createElement('span'); s.className = 'calendar-onekite-badge calendar-onekite-badge--pending'; badges.appendChild(s); }
|
|
999
|
-
if (b.paid) { const s = document.createElement('span'); s.className = 'calendar-onekite-badge calendar-onekite-badge--paid'; badges.appendChild(s); }
|
|
1000
|
-
if (showSpecial && b.special) { const s = document.createElement('span'); s.className = 'calendar-onekite-badge calendar-onekite-badge--special'; badges.appendChild(s); }
|
|
1001
|
-
cell.appendChild(badges);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
grid.appendChild(cell);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
monthEl.innerHTML = '';
|
|
1008
|
-
monthEl.appendChild(grid);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// Re-render widgets on every page load/end.
|
|
1013
|
-
if (hooks && typeof hooks.on === 'function') {
|
|
1014
|
-
hooks.on('action:ajaxify.end', () => { renderMiniWidgets().catch(() => {}); });
|
|
1015
|
-
}
|
|
1016
|
-
setTimeout(() => { renderMiniWidgets().catch(() => {}); }, 0);
|
|
1017
|
-
|
|
1018
916
|
|
|
1019
917
|
|
|
1020
918
|
// Live refresh when a reservation changes (e.g., payment confirmed by webhook)
|
|
@@ -6,26 +6,27 @@
|
|
|
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"
|
|
9
|
+
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#onekite-tab-settings" type="button" role="tab">Locations</button>
|
|
10
10
|
</li>
|
|
11
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"
|
|
12
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-events" type="button" role="tab">Évènements</button>
|
|
13
13
|
</li>
|
|
14
14
|
<li class="nav-item" role="presentation">
|
|
15
|
-
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-pending" type="button" role="tab"
|
|
15
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-pending" type="button" role="tab">Demandes en attente</button>
|
|
16
16
|
</li>
|
|
17
17
|
<li class="nav-item" role="presentation">
|
|
18
|
-
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-debug" type="button" role="tab"
|
|
18
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-debug" type="button" role="tab">Debug HelloAsso</button>
|
|
19
19
|
</li>
|
|
20
20
|
<li class="nav-item" role="presentation">
|
|
21
|
-
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-accounting" type="button" role="tab"
|
|
21
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-accounting" type="button" role="tab">Comptabilisation</button>
|
|
22
22
|
</li>
|
|
23
23
|
</ul>
|
|
24
24
|
|
|
25
25
|
<div class="tab-content pt-3">
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
<div class="tab-pane fade show active" id="onekite-tab-settings" role="tabpanel">
|
|
28
|
-
|
|
28
|
+
<form id="onekite-settings-form" class="mt-1">
|
|
29
|
+
<h4>Groupes</h4>
|
|
29
30
|
<div class="mb-3">
|
|
30
31
|
<label class="form-label">Groupes autorisés à créer une demande (csv)</label>
|
|
31
32
|
<input class="form-control" name="creatorGroups" placeholder="ex: registered-users,membres">
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
<label class="form-label">Form Slug</label>
|
|
88
89
|
<input class="form-control" name="helloassoFormSlug">
|
|
89
90
|
</div>
|
|
91
|
+
</form>
|
|
90
92
|
</div>
|
|
91
93
|
|
|
92
94
|
<div class="tab-pane fade" id="onekite-tab-events" role="tabpanel">
|
|
@@ -112,7 +114,7 @@
|
|
|
112
114
|
<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>
|
|
113
115
|
</div>
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
|
|
116
118
|
|
|
117
119
|
<div class="tab-pane fade" id="onekite-tab-pending" role="tabpanel">
|
|
118
120
|
<h4>Demandes en attente</h4>
|
|
@@ -183,4 +185,4 @@
|
|
|
183
185
|
});
|
|
184
186
|
</script>
|
|
185
187
|
|
|
186
|
-
<!-- IMPORT admin/partials/settings/footer.tpl -->
|
|
188
|
+
<!-- IMPORT admin/partials/settings/footer.tpl -->
|