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 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's Emailer API differs across versions; the most reliable approach is to pass `subject` inside `data`.
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
- // Prefer 3-args (template, email, data) when possible
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
- if (emailer.send.length >= 3) {
33
- await emailer.send(template, toEmail, dataWithSubject);
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
- await emailer.send(template, toEmail, subject, data);
37
- return;
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((err && err.message) || err) });
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
- color: '#8e44ad',
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
- if (!toEmail) return;
60
- try {
61
- if (typeof emailer.sendToEmail === 'function') {
62
- await emailer.sendToEmail(template, toEmail, subject, data);
63
- return;
64
- }
65
- if (typeof emailer.send === 'function') {
66
- if (emailer.send.length >= 4) {
67
- await emailer.send(template, toEmail, subject, data);
68
- return;
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
- function formatFR(ts) {
83
- const d = new Date(ts);
84
- const dd = String(d.getDate()).padStart(2, '0');
85
- const mm = String(d.getMonth() + 1).padStart(2, '0');
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
- await dbLayer.removeReservation(rid);
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, '&amp;')
25
- .replace(/</g, '&lt;')
26
- .replace(/>/g, '&gt;')
27
- .replace(/"/g, '&quot;')
28
- .replace(/'/g, '&#39;');
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.85",
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.49"
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
- let end = selectionInfo.end;
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
- const dlg = bootbox.dialog({
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 once the modal is visible (Leaflet needs a laid-out container)
90
- dlg.on('shown.bs.modal', async () => {
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: '&copy; 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
- const dlg = bootbox.dialog({
128
+ bootbox.dialog({
141
129
  title: title || 'Carte',
142
130
  message: html,
143
131
  buttons: { close: { label: 'Fermer', className: 'btn-secondary' } },
144
132
  });
145
- dlg.on('shown.bs.modal', async () => {
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-only strings from FullCalendar to avoid timezone shifts.
556
- // For all-day selections, startStr/endStr are YYYY-MM-DD (end is exclusive).
557
- const startDate = info.startStr;
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(formatDt(ev.start))} → ${escapeHtml(formatDt(ev.end))}</div>
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" aria-controls="onekite-tab-settings" aria-selected="true">Locations</button>
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" aria-controls="onekite-tab-events" aria-selected="false">Évènements</button>
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" aria-controls="onekite-tab-pending" aria-selected="false">Demandes en attente</button>
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" aria-controls="onekite-tab-debug" aria-selected="false">Debug HelloAsso</button>
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" aria-controls="onekite-tab-accounting" aria-selected="false">Comptabilisation</button>
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
- <form id="onekite-settings-form" class="mt-1">
26
+
27
27
  <div class="tab-pane fade show active" id="onekite-tab-settings" role="tabpanel">
28
- <h4>Groupes</h4>
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
- </form>
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 -->