nodebb-plugin-calendar-onekite 11.1.25 → 11.1.26
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/library.js +1 -0
- package/package.json +1 -1
- package/public/client.js +117 -240
- package/templates/calendar-onekite.tpl +0 -3
- package/README.md +0 -38
- package/lib/controllers.js +0 -11
- package/lib/scheduler.js +0 -50
- package/lib/settings.js +0 -36
- package/public/calendar-onekite.scss +0 -8
- package/templates/calendar-onekite/calendar.tpl +0 -29
- package/templates/emails/calendar-onekite_approved.tpl +0 -10
- package/templates/emails/calendar-onekite_pending.tpl +0 -10
- package/templates/emails/calendar-onekite_refused.tpl +0 -7
package/library.js
CHANGED
|
@@ -101,6 +101,7 @@ Plugin.init = async function (params) {
|
|
|
101
101
|
|
|
102
102
|
// Add to admin navigation
|
|
103
103
|
Plugin.addAdminNavigation = async function (data) {
|
|
104
|
+
data.plugins = data.plugins || [];
|
|
104
105
|
data.plugins.push({
|
|
105
106
|
route: '/plugins/calendar-onekite',
|
|
106
107
|
icon: 'fa-calendar',
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1,265 +1,139 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
3
2
|
/* global FullCalendar */
|
|
4
3
|
|
|
5
|
-
define('forum/calendar-onekite', ['api', 'alerts'
|
|
4
|
+
define('forum/calendar-onekite', ['api', 'alerts'], function (api, alerts) {
|
|
6
5
|
let calendar;
|
|
7
6
|
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
const [y, m, d] = ymd.split('-').map(Number);
|
|
11
|
-
return Date.UTC(y, m - 1, d, 0, 0, 0, 0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function daysBetween(startYmd, endYmd) {
|
|
15
|
-
const start = ymdToTs(startYmd);
|
|
16
|
-
const end = ymdToTs(endYmd);
|
|
17
|
-
const dayMs = 24 * 3600 * 1000;
|
|
18
|
-
return Math.max(1, Math.round((end - start) / dayMs));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function fetchJson(url, opts) {
|
|
22
|
-
try {
|
|
23
|
-
return await api[opts && opts.method === 'POST' ? 'post' : 'get'](url, opts && opts.body ? opts.body : undefined);
|
|
24
|
-
} catch (e) {
|
|
25
|
-
// fallback to fetch if api module not compatible
|
|
26
|
-
const res = await fetch(url, {
|
|
27
|
-
method: (opts && opts.method) || 'GET',
|
|
28
|
-
headers: { 'Content-Type': 'application/json' },
|
|
29
|
-
body: opts && opts.body ? JSON.stringify(opts.body) : undefined,
|
|
30
|
-
credentials: 'same-origin',
|
|
31
|
-
});
|
|
32
|
-
const j = await res.json().catch(() => ({}));
|
|
33
|
-
if (!res.ok) throw Object.assign(new Error('http'), { status: res.status, body: j });
|
|
34
|
-
return j;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function loadItems() {
|
|
39
|
-
const r = await fetchJson('/plugins/calendar-onekite/items');
|
|
40
|
-
return (r && r.items) || [];
|
|
7
|
+
function showError(msg) {
|
|
8
|
+
try { alerts.error(msg); } catch (e) { console.error(msg); }
|
|
41
9
|
}
|
|
42
10
|
|
|
43
|
-
function
|
|
44
|
-
|
|
11
|
+
function daysBetween(startStr, endStr) {
|
|
12
|
+
// start/end are YYYY-MM-DD strings, end is exclusive
|
|
13
|
+
const s = new Date(startStr + 'T00:00:00Z');
|
|
14
|
+
const e = new Date(endStr + 'T00:00:00Z');
|
|
15
|
+
const ms = e.getTime() - s.getTime();
|
|
16
|
+
return Math.max(1, Math.round(ms / (24 * 3600 * 1000)));
|
|
45
17
|
}
|
|
46
18
|
|
|
47
|
-
function
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
<input type="checkbox" class="onekite-item" value="${String(it.id).replace(/"/g, '"')}">
|
|
51
|
-
<span style="flex:1;">${String(it.name)}</span>
|
|
52
|
-
<span>${formatEuro(it.priceCents)}</span>
|
|
53
|
-
</label>`;
|
|
54
|
-
}).join('');
|
|
55
|
-
return `<div id="onekite-items">${rows}</div>`;
|
|
19
|
+
async function fetchCatalog() {
|
|
20
|
+
const res = await api.get('/plugins/calendar-onekite/items');
|
|
21
|
+
return (res && res.items) || [];
|
|
56
22
|
}
|
|
57
23
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
function sumSelected(items, ids) {
|
|
63
|
-
const map = new Map(items.map(i => [String(i.id), i]));
|
|
64
|
-
return ids.reduce((s, id) => s + Number((map.get(String(id)) || {}).priceCents || 0), 0);
|
|
24
|
+
function formatEuroFromCents(cents) {
|
|
25
|
+
const v = (Number(cents || 0) / 100).toFixed(2).replace('.', ',');
|
|
26
|
+
return v + ' €';
|
|
65
27
|
}
|
|
66
28
|
|
|
67
29
|
async function openRequestModal(selectionInfo) {
|
|
68
|
-
const startYmd = selectionInfo.startStr;
|
|
69
|
-
const endYmd = selectionInfo.endStr; // exclusive
|
|
70
|
-
const days = daysBetween(startYmd, endYmd);
|
|
71
|
-
|
|
72
|
-
let items = [];
|
|
73
30
|
try {
|
|
74
|
-
items = await
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!items.length) {
|
|
82
|
-
alerts.error('Aucun matériel disponible (catalogue HelloAsso vide).');
|
|
83
|
-
calendar.unselect();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const body = document.createElement('div');
|
|
88
|
-
body.innerHTML = `
|
|
89
|
-
<p><strong>Période :</strong> ${startYmd} → ${endYmd} (${days} jour(s))</p>
|
|
90
|
-
<p><strong>Matériel :</strong></p>
|
|
91
|
-
${buildItemsList(items)}
|
|
92
|
-
<hr>
|
|
93
|
-
<p><strong>Total estimé :</strong> <span id="onekite-total">${formatEuro(0)}</span></p>
|
|
94
|
-
<p class="help-block">Astuce : coche plusieurs matériels. Ctrl/Cmd+clic sur le libellé fonctionne aussi.</p>
|
|
95
|
-
`;
|
|
96
|
-
|
|
97
|
-
const updateTotal = () => {
|
|
98
|
-
const ids = getSelectedIds(body);
|
|
99
|
-
const perDay = sumSelected(items, ids);
|
|
100
|
-
body.querySelector('#onekite-total').textContent = formatEuro(perDay * days);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
body.addEventListener('change', (e) => {
|
|
104
|
-
if (e.target && e.target.classList.contains('onekite-item')) updateTotal();
|
|
105
|
-
});
|
|
106
|
-
body.addEventListener('click', (e) => {
|
|
107
|
-
const label = e.target.closest('label');
|
|
108
|
-
if (!label) return;
|
|
109
|
-
const cb = label.querySelector('input.onekite-item');
|
|
110
|
-
if (!cb) return;
|
|
111
|
-
if (e.ctrlKey || e.metaKey) {
|
|
112
|
-
cb.checked = !cb.checked;
|
|
113
|
-
updateTotal();
|
|
31
|
+
const items = await fetchCatalog();
|
|
32
|
+
if (!items.length) {
|
|
33
|
+
showError('Aucun matériel disponible (catalogue HelloAsso non chargé).');
|
|
34
|
+
return;
|
|
114
35
|
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
updateTotal();
|
|
118
36
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
calendar.unselect();
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
}
|
|
37
|
+
const start = selectionInfo.startStr;
|
|
38
|
+
const end = selectionInfo.endStr;
|
|
39
|
+
const nbDays = daysBetween(start, end);
|
|
40
|
+
|
|
41
|
+
// Build simple modal UI (no jquery)
|
|
42
|
+
const modal = document.createElement('div');
|
|
43
|
+
modal.className = 'onekite-modal';
|
|
44
|
+
modal.innerHTML = `
|
|
45
|
+
<div class="onekite-modal__backdrop"></div>
|
|
46
|
+
<div class="onekite-modal__panel">
|
|
47
|
+
<h4>Nouvelle demande</h4>
|
|
48
|
+
<div style="margin-bottom:8px;">Du <strong>${start}</strong> au <strong>${end}</strong> (<strong>${nbDays}</strong> jour(s))</div>
|
|
49
|
+
<div style="max-height:240px; overflow:auto; border:1px solid #ddd; padding:8px; border-radius:6px;">
|
|
50
|
+
${items.map((it, idx) => `
|
|
51
|
+
<label style="display:flex; align-items:center; gap:8px; margin:6px 0; cursor:pointer;">
|
|
52
|
+
<input type="checkbox" class="onekite-item" data-id="${it.id}" data-price="${it.priceCents || 0}">
|
|
53
|
+
<span>${it.name}</span>
|
|
54
|
+
<span style="margin-left:auto; opacity:.8;">${formatEuroFromCents(it.priceCents || 0)}/jour</span>
|
|
55
|
+
</label>
|
|
56
|
+
`).join('')}
|
|
57
|
+
</div>
|
|
58
|
+
<div style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
|
|
59
|
+
<div>Estimation total : <strong id="onekite-total">0 €</strong></div>
|
|
60
|
+
<div style="display:flex; gap:8px;">
|
|
61
|
+
<button class="btn btn-default" id="onekite-cancel">Annuler</button>
|
|
62
|
+
<button class="btn btn-primary" id="onekite-submit">Envoyer</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
`;
|
|
67
|
+
document.body.appendChild(modal);
|
|
68
|
+
|
|
69
|
+
const updateTotal = () => {
|
|
70
|
+
const checks = Array.from(modal.querySelectorAll('.onekite-item')).filter(i => i.checked);
|
|
71
|
+
const daily = checks.reduce((sum, el) => sum + Number(el.dataset.price || 0), 0);
|
|
72
|
+
const total = daily * nbDays;
|
|
73
|
+
modal.querySelector('#onekite-total').textContent = formatEuroFromCents(total);
|
|
74
|
+
};
|
|
75
|
+
modal.querySelectorAll('.onekite-item').forEach(el => el.addEventListener('change', updateTotal));
|
|
76
|
+
updateTotal();
|
|
167
77
|
|
|
168
|
-
|
|
169
|
-
const ev = info.event;
|
|
170
|
-
const props = ev.extendedProps || {};
|
|
171
|
-
const status = props.status || 'pending';
|
|
172
|
-
const items = (props.items || []).map(i => i.name).join(', ');
|
|
173
|
-
const days = props.days || '';
|
|
174
|
-
const total = props.totalCents != null ? formatEuro(props.totalCents) : '';
|
|
78
|
+
const close = () => { modal.remove(); calendar && calendar.unselect(); };
|
|
175
79
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<p><strong>Statut :</strong> ${status}</p>
|
|
179
|
-
<p><strong>Matériel :</strong> ${items}</p>
|
|
180
|
-
${days ? `<p><strong>Jours :</strong> ${days}</p>` : ''}
|
|
181
|
-
${total ? `<p><strong>Total :</strong> ${total}</p>` : ''}
|
|
182
|
-
`;
|
|
80
|
+
modal.querySelector('#onekite-cancel').addEventListener('click', close);
|
|
81
|
+
modal.querySelector('.onekite-modal__backdrop').addEventListener('click', close);
|
|
183
82
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
buttons.approve = {
|
|
207
|
-
label: 'Valider',
|
|
208
|
-
className: 'btn-success',
|
|
209
|
-
callback: async function () {
|
|
210
|
-
try {
|
|
211
|
-
const r = await fetchJson(`/plugins/calendar-onekite/reservations/${encodeURIComponent(ev.id)}/approve`, { method: 'POST', body: {} });
|
|
212
|
-
alerts.success('Réservation validée.');
|
|
213
|
-
if (r && r.paymentUrl) {
|
|
214
|
-
alerts.success('Lien de paiement envoyé.');
|
|
215
|
-
}
|
|
216
|
-
calendar.refetchEvents();
|
|
217
|
-
} catch (e) {
|
|
218
|
-
alerts.error("Impossible de valider (droits requis).");
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
};
|
|
83
|
+
modal.querySelector('#onekite-submit').addEventListener('click', async () => {
|
|
84
|
+
const selected = Array.from(modal.querySelectorAll('.onekite-item')).filter(i => i.checked).map(i => i.dataset.id);
|
|
85
|
+
if (!selected.length) {
|
|
86
|
+
showError('Sélectionne au moins un matériel.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await api.post('/plugins/calendar-onekite/reservations', { start, end, itemIds: selected });
|
|
91
|
+
try { alerts.success('Demande envoyée'); } catch {}
|
|
92
|
+
close();
|
|
93
|
+
await calendar.refetchEvents();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const msg = (err && err.message) ? err.message : 'Impossible de créer la demande';
|
|
96
|
+
showError(msg);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
showError('Impossible de charger le catalogue.');
|
|
101
|
+
console.error(e);
|
|
222
102
|
}
|
|
223
|
-
|
|
224
|
-
bootbox.dialog({
|
|
225
|
-
title: 'Réservation',
|
|
226
|
-
message: body,
|
|
227
|
-
buttons,
|
|
228
|
-
});
|
|
229
103
|
}
|
|
230
104
|
|
|
231
|
-
function initCalendar() {
|
|
105
|
+
async function initCalendar() {
|
|
232
106
|
const el = document.getElementById('onekite-calendar');
|
|
233
|
-
if (!el ||
|
|
107
|
+
if (!el || typeof FullCalendar === 'undefined') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
234
110
|
|
|
235
111
|
calendar = new FullCalendar.Calendar(el, {
|
|
236
|
-
locale: 'fr',
|
|
237
112
|
initialView: 'dayGridMonth',
|
|
113
|
+
locale: 'fr',
|
|
238
114
|
selectable: true,
|
|
239
115
|
selectMirror: true,
|
|
240
116
|
displayEventTime: false,
|
|
241
|
-
|
|
242
|
-
select:
|
|
243
|
-
|
|
244
|
-
},
|
|
245
|
-
eventClick: function (info) {
|
|
246
|
-
info.jsEvent.preventDefault();
|
|
247
|
-
openEventModal(info);
|
|
248
|
-
},
|
|
249
|
-
events: async function (fetchInfo, success, failure) {
|
|
117
|
+
eventTimeFormat: { hour: '2-digit', minute: '2-digit' },
|
|
118
|
+
select: (info) => openRequestModal(info),
|
|
119
|
+
events: async (fetchInfo, successCallback, failureCallback) => {
|
|
250
120
|
try {
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
121
|
+
const qs = new URLSearchParams({ start: fetchInfo.startStr, end: fetchInfo.endStr });
|
|
122
|
+
const res = await fetch('/api/v3/plugins/calendar-onekite/events?' + qs.toString(), { credentials: 'same-origin' });
|
|
123
|
+
if (!res.ok) throw new Error('events-fetch-failed');
|
|
124
|
+
const json = await res.json();
|
|
125
|
+
const events = (json.events || []).map(ev => ({
|
|
126
|
+
id: ev.id,
|
|
127
|
+
title: ev.title,
|
|
128
|
+
start: ev.start,
|
|
129
|
+
end: ev.end,
|
|
130
|
+
allDay: true,
|
|
131
|
+
extendedProps: ev.extendedProps || {},
|
|
132
|
+
}));
|
|
133
|
+
successCallback(events);
|
|
255
134
|
} catch (e) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
eventDidMount: function (arg) {
|
|
260
|
-
const st = (arg.event.extendedProps && arg.event.extendedProps.status) || '';
|
|
261
|
-
if (st === 'pending' || st === 'awaiting_payment') {
|
|
262
|
-
arg.el.title = 'En attente';
|
|
135
|
+
console.error(e);
|
|
136
|
+
failureCallback(e);
|
|
263
137
|
}
|
|
264
138
|
},
|
|
265
139
|
});
|
|
@@ -267,19 +141,22 @@ define('forum/calendar-onekite', ['api', 'alerts', 'hooks', 'bootbox'], function
|
|
|
267
141
|
calendar.render();
|
|
268
142
|
}
|
|
269
143
|
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
} catch (e) { /* ignore */ }
|
|
275
|
-
initCalendar();
|
|
144
|
+
function onAjaxifyEnd(_ev, data) {
|
|
145
|
+
if (data && data.tpl === 'calendar-onekite') {
|
|
146
|
+
initCalendar().catch(console.error);
|
|
147
|
+
}
|
|
276
148
|
}
|
|
277
149
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
150
|
+
// Auto-init on page load & ajaxify
|
|
151
|
+
if (window && window.addEventListener) {
|
|
152
|
+
window.addEventListener('action:ajaxify.end', (ev) => onAjaxifyEnd(ev, ev && ev.detail ? ev.detail : undefined));
|
|
153
|
+
}
|
|
154
|
+
// NodeBB uses jQuery-triggered events too, but jQuery isn't guaranteed. If present, hook it.
|
|
155
|
+
if (typeof window !== 'undefined' && window.jQuery) {
|
|
156
|
+
window.jQuery(window).on('action:ajaxify.end', function (_ev, data) { onAjaxifyEnd(_ev, data); });
|
|
157
|
+
}
|
|
281
158
|
|
|
282
159
|
return {
|
|
283
|
-
init:
|
|
160
|
+
init: initCalendar,
|
|
284
161
|
};
|
|
285
162
|
});
|
|
@@ -11,6 +11,3 @@
|
|
|
11
11
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script>
|
|
12
12
|
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.11/locales-all.global.min.js"></script>
|
|
13
13
|
|
|
14
|
-
<script>
|
|
15
|
-
require(['forum/calendar-onekite'], function (mod) { mod.init(); });
|
|
16
|
-
</script>
|
package/README.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# nodebb-plugin-calendar-onekite
|
|
2
|
-
|
|
3
|
-
Plugin NodeBB (v4.7.x) : calendrier de réservation de matériel basé sur FullCalendar, workflow de validation via ACP, et génération de lien de paiement HelloAsso.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
Dans le dossier NodeBB :
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install /path/to/nodebb-plugin-calendar-onekite
|
|
11
|
-
# ou si vous avez un zip, dézippez dans node_modules puis :
|
|
12
|
-
./nodebb build
|
|
13
|
-
./nodebb restart
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Activez ensuite le plugin dans l’ACP.
|
|
17
|
-
|
|
18
|
-
## Page
|
|
19
|
-
|
|
20
|
-
- URL : `/calendar` (ex: https://www.onekite.com/calendar)
|
|
21
|
-
|
|
22
|
-
## Paramètres ACP
|
|
23
|
-
|
|
24
|
-
ACP → Plugins → Calendar OneKite
|
|
25
|
-
|
|
26
|
-
- `allowedGroups` : groupes autorisés à créer une demande (séparés par virgules)
|
|
27
|
-
- `notifyGroups` : groupes notifiés par email
|
|
28
|
-
- `pendingHoldMinutes` : durée de blocage d’une demande en attente
|
|
29
|
-
- HelloAsso :
|
|
30
|
-
- `helloassoEnv` : sandbox/prod
|
|
31
|
-
- `helloassoClientId` / `helloassoClientSecret`
|
|
32
|
-
- `helloassoOrganizationSlug` / `helloassoFormType` / `helloassoFormSlug`
|
|
33
|
-
- `helloassoReturnUrl` (optionnel)
|
|
34
|
-
|
|
35
|
-
## Notes
|
|
36
|
-
|
|
37
|
-
- Les demandes sont créées en statut `pending` puis expirent automatiquement après `pendingHoldMinutes`.
|
|
38
|
-
- La validation admin crée un checkout-intent HelloAsso et envoie le lien de paiement au demandeur.
|
package/lib/controllers.js
DELETED
package/lib/scheduler.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const meta = require.main.require('./src/meta');
|
|
4
|
-
const dbLayer = require('./db');
|
|
5
|
-
|
|
6
|
-
let timer = null;
|
|
7
|
-
|
|
8
|
-
async function expirePending() {
|
|
9
|
-
const settings = await meta.settings.get('calendar-onekite');
|
|
10
|
-
const holdMins = parseInt(settings.pendingHoldMinutes || '5', 10) || 5;
|
|
11
|
-
const now = Date.now();
|
|
12
|
-
|
|
13
|
-
const ids = await dbLayer.listAllReservationIds(5000);
|
|
14
|
-
if (!ids || !ids.length) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
for (const rid of ids) {
|
|
19
|
-
const resv = await dbLayer.getReservation(rid);
|
|
20
|
-
if (!resv || resv.status !== 'pending') {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const createdAt = parseInt(resv.createdAt, 10) || 0;
|
|
24
|
-
const expiresAt = createdAt + holdMins * 60 * 1000;
|
|
25
|
-
if (now > expiresAt) {
|
|
26
|
-
// Expire (remove from calendar)
|
|
27
|
-
await dbLayer.removeReservation(rid);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function start() {
|
|
33
|
-
if (timer) return;
|
|
34
|
-
timer = setInterval(() => {
|
|
35
|
-
expirePending().catch(() => {});
|
|
36
|
-
}, 60 * 1000);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function stop() {
|
|
40
|
-
if (timer) {
|
|
41
|
-
clearInterval(timer);
|
|
42
|
-
timer = null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = {
|
|
47
|
-
start,
|
|
48
|
-
stop,
|
|
49
|
-
expirePending,
|
|
50
|
-
};
|
package/lib/settings.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const Settings = require.main.require('./src/settings');
|
|
4
|
-
|
|
5
|
-
const defaults = {
|
|
6
|
-
// Who can create reservations (comma-separated group names)
|
|
7
|
-
allowedGroups: 'registered-users',
|
|
8
|
-
// Who receives email notifications for new pending reservations (comma-separated group names)
|
|
9
|
-
notifyGroups: 'administrators',
|
|
10
|
-
|
|
11
|
-
// How long a "pending" reservation blocks equipment (minutes)
|
|
12
|
-
pendingHoldMinutes: 5,
|
|
13
|
-
|
|
14
|
-
// Default reservation status for newly created (pending)
|
|
15
|
-
// Also used by cleanup job
|
|
16
|
-
cleanupIntervalSeconds: 60,
|
|
17
|
-
|
|
18
|
-
// HelloAsso
|
|
19
|
-
helloassoEnv: 'sandbox', // sandbox | prod
|
|
20
|
-
helloassoClientId: '',
|
|
21
|
-
helloassoClientSecret: '',
|
|
22
|
-
helloassoOrganizationSlug: '',
|
|
23
|
-
helloassoFormType: 'event', // event | membership | donation | crowdfunding | paymentform
|
|
24
|
-
helloassoFormSlug: '',
|
|
25
|
-
helloassoReturnUrl: '',
|
|
26
|
-
|
|
27
|
-
// UX
|
|
28
|
-
showUsernamesOnCalendar: false,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const settings = new Settings('calendar-onekite', '1.0.0', defaults);
|
|
32
|
-
|
|
33
|
-
module.exports = {
|
|
34
|
-
settings,
|
|
35
|
-
defaults,
|
|
36
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<div class="container">
|
|
2
|
-
<div class="row">
|
|
3
|
-
<div class="col-12">
|
|
4
|
-
<h1>Calendrier des réservations</h1>
|
|
5
|
-
<p class="text-muted">Cliquez sur une date, ou sélectionnez une plage, pour faire une demande de réservation.</p>
|
|
6
|
-
<div id="onekite-calendar"></div>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
9
|
-
</div>
|
|
10
|
-
|
|
11
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.css" />
|
|
12
|
-
|
|
13
|
-
<script defer src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script>
|
|
14
|
-
<script defer src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.11/locales-all.global.min.js"></script>
|
|
15
|
-
|
|
16
|
-
<script>
|
|
17
|
-
(function () {
|
|
18
|
-
function boot() {
|
|
19
|
-
if (!window.require) return setTimeout(boot, 50);
|
|
20
|
-
window.require(['forum/calendar-onekite'], function (CalendarOneKite) {
|
|
21
|
-
CalendarOneKite.init({
|
|
22
|
-
el: '#onekite-calendar',
|
|
23
|
-
locale: (ajaxify && ajaxify.data && ajaxify.data.config && ajaxify.data.config.userLang) || 'fr',
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
boot();
|
|
28
|
-
})();
|
|
29
|
-
</script>
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<p>Bonjour {username},</p>
|
|
2
|
-
<p>Votre réservation a été validée.</p>
|
|
3
|
-
<ul>
|
|
4
|
-
<li>Matériel : {itemName}</li>
|
|
5
|
-
<li>Début : {start}</li>
|
|
6
|
-
<li>Fin : {end}</li>
|
|
7
|
-
</ul>
|
|
8
|
-
<!-- IF paymentUrl -->
|
|
9
|
-
<p>Lien de paiement : <a href="{paymentUrl}">{paymentUrl}</a></p>
|
|
10
|
-
<!-- ENDIF paymentUrl -->
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<p>Bonjour {username},</p>
|
|
2
|
-
<p>Nouvelle demande de réservation :</p>
|
|
3
|
-
<ul>
|
|
4
|
-
<li>Demandeur : {requester}</li>
|
|
5
|
-
<li>Matériel : {itemName}</li>
|
|
6
|
-
<li>Début : {start}</li>
|
|
7
|
-
<li>Fin : {end}</li>
|
|
8
|
-
<li>ID : {rid}</li>
|
|
9
|
-
</ul>
|
|
10
|
-
<p>Validation/refus via l’ACP.</p>
|