nodebb-plugin-calendar-onekite 2.1.1 → 2.1.2
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/package.json +1 -1
- package/plugin.json +3 -2
- package/static/js/admin-planning.bundle.js +26 -0
- package/static/js/admin.bundle.js +14 -236
- package/static/js/calendar.bundle.js +47 -253
- package/templates/admin/calendar-planning.tpl +2 -6
- package/templates/admin/plugins/calendar-onekite.tpl +0 -3
- package/templates/calendar.tpl +0 -5
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
"name": "Calendar Onekite",
|
|
4
4
|
"description": "Calendrier + réservation matériel + validation admin + paiement HelloAsso pour NodeBB v4 (no-jQuery UI)",
|
|
5
5
|
"url": "",
|
|
6
|
-
"version": "2.1.
|
|
6
|
+
"version": "2.1.2",
|
|
7
7
|
"library": "./library.js",
|
|
8
8
|
"staticDirs": {
|
|
9
9
|
"static": "static"
|
|
10
10
|
},
|
|
11
11
|
"acpScripts": [
|
|
12
|
-
"static/js/admin.bundle.js"
|
|
12
|
+
"static/js/admin.bundle.js",
|
|
13
|
+
"static/js/admin-planning.bundle.js"
|
|
13
14
|
],
|
|
14
15
|
"hooks": [
|
|
15
16
|
{
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function ready(fn){
|
|
5
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
|
6
|
+
else fn();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function boot(){
|
|
10
|
+
if (!document.querySelector('#planning-calendar')) return;
|
|
11
|
+
|
|
12
|
+
if (typeof window.require !== 'function') {
|
|
13
|
+
console.warn('[calendar-onekite] require is not defined (planning)');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
window.require([
|
|
18
|
+
'plugins/nodebb-plugin-calendar-onekite/static/js/admin-planning'
|
|
19
|
+
], function (Mod) {
|
|
20
|
+
try { Mod && Mod.init && Mod.init(); } catch (e) { console.error(e); }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ready(boot);
|
|
25
|
+
document.addEventListener('action:ajaxify.end', boot);
|
|
26
|
+
})();
|
|
@@ -1,248 +1,26 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
|
-
if (window.__onekiteAdminBundleLoaded) { return; }
|
|
4
|
-
window.__onekiteAdminBundleLoaded = true;
|
|
5
3
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
function escapeHtml(s) {
|
|
9
|
-
return String(s ?? '')
|
|
10
|
-
.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>')
|
|
11
|
-
.replaceAll('"','"').replaceAll("'","'");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function onReady(fn) {
|
|
15
|
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
4
|
+
function ready(fn){
|
|
5
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
|
16
6
|
else fn();
|
|
17
7
|
}
|
|
18
8
|
|
|
19
|
-
function boot(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
async function saveSettings() {
|
|
23
|
-
const locationsJson = qs('#calendar-onekite-locations')?.value || '[]';
|
|
24
|
-
const inventoryJson = qs('#calendar-onekite-inventory')?.value || '[]';
|
|
25
|
-
|
|
26
|
-
try { JSON.parse(locationsJson || '[]'); } catch { return alerts.error('JSON Lieux invalide'); }
|
|
27
|
-
try { JSON.parse(inventoryJson || '[]'); } catch { return alerts.error('JSON Inventaire invalide'); }
|
|
9
|
+
function boot(){
|
|
10
|
+
if (!document.querySelector('#calendar-onekite-admin') && !document.querySelector('#calendar-planning')) return;
|
|
28
11
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
limit: qs('#calendar-onekite-widget-limit')?.value || '5',
|
|
33
|
-
locationsJson,
|
|
34
|
-
inventoryJson,
|
|
35
|
-
helloassoApiBase: qs('#calendar-onekite-helloasso-apibase')?.value || '',
|
|
36
|
-
helloassoOrganizationSlug: qs('#calendar-onekite-helloasso-org')?.value || '',
|
|
37
|
-
helloassoFormSlug: qs('#calendar-onekite-helloasso-form')?.value || '',
|
|
38
|
-
helloassoClientId: qs('#calendar-onekite-helloasso-clientid')?.value || '',
|
|
39
|
-
helloassoClientSecret: qs('#calendar-onekite-helloasso-secret')?.value || '',
|
|
40
|
-
helloassoReturnUrl: qs('#calendar-onekite-helloasso-return')?.value || '',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
await api.put('/admin/plugins/calendar-onekite', settings);
|
|
45
|
-
alerts.success('Paramètres enregistrés');
|
|
46
|
-
} catch (e) {
|
|
47
|
-
console.error('[calendar-onekite] saveSettings error', e);
|
|
48
|
-
alerts.error(e?.message || e);
|
|
49
|
-
}
|
|
12
|
+
if (typeof window.require !== 'function') {
|
|
13
|
+
console.warn('[calendar-onekite] require is not defined (ACP)');
|
|
14
|
+
return;
|
|
50
15
|
}
|
|
51
16
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Bind button
|
|
57
|
-
const btn = qs('#calendar-onekite-save');
|
|
58
|
-
if (btn && !btn.__bound) {
|
|
59
|
-
btn.__bound = true;
|
|
60
|
-
btn.addEventListener('click', saveSettings);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Delegation for pending actions
|
|
64
|
-
if (!root.__delegationBound) {
|
|
65
|
-
root.__delegationBound = true;
|
|
66
|
-
root.addEventListener('click', async (ev) => {
|
|
67
|
-
const t = ev.target;
|
|
68
|
-
if (!(t instanceof HTMLElement)) return;
|
|
69
|
-
const row = t.closest('.reservation-row');
|
|
70
|
-
if (!row) return;
|
|
71
|
-
const rid = row.getAttribute('data-rid');
|
|
72
|
-
if (!rid) return;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
if (t.classList.contains('js-validate')) {
|
|
76
|
-
await api.post(`/admin/calendar/reservation/${encodeURIComponent(rid)}/validate`, {});
|
|
77
|
-
row.style.background = '#d4edda';
|
|
78
|
-
qsa('.js-validate, .js-cancel', row).forEach(b => b.remove());
|
|
79
|
-
row.insertAdjacentHTML('beforeend', '<p><strong>Validée.</strong> Lien de paiement envoyé.</p>');
|
|
80
|
-
}
|
|
81
|
-
if (t.classList.contains('js-cancel')) {
|
|
82
|
-
if (!window.confirm('Annuler cette réservation ?')) return;
|
|
83
|
-
await api.post(`/admin/calendar/reservation/${encodeURIComponent(rid)}/cancel`, {});
|
|
84
|
-
row.style.background = '#f8d7da';
|
|
85
|
-
qsa('.js-validate, .js-cancel', row).forEach(b => b.remove());
|
|
86
|
-
row.insertAdjacentHTML('beforeend', '<p><strong>Annulée.</strong></p>');
|
|
87
|
-
}
|
|
88
|
-
} catch (e) {
|
|
89
|
-
console.error('[calendar-onekite] pending action error', e);
|
|
90
|
-
alerts.error(e?.message || e);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
loadPending();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function loadPending() {
|
|
99
|
-
const container = qs('#pending-reservations');
|
|
100
|
-
if (!container) return;
|
|
101
|
-
try {
|
|
102
|
-
const list = await api.get('/admin/calendar/pending');
|
|
103
|
-
if (!Array.isArray(list) || !list.length) {
|
|
104
|
-
container.innerHTML = '<p>Aucune réservation en attente.</p>';
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
container.innerHTML = list.map(block => {
|
|
108
|
-
const ev = block.event || {};
|
|
109
|
-
const reservations = Array.isArray(block.reservations) ? block.reservations : [];
|
|
110
|
-
const rows = reservations.map(r => `
|
|
111
|
-
<div class="reservation-row mb-2 p-2 border rounded" data-rid="${escapeHtml(r.rid)}">
|
|
112
|
-
<p><strong>RID :</strong> ${escapeHtml(r.rid)}</p>
|
|
113
|
-
<p><strong>UID :</strong> ${escapeHtml(r.uid)}</p>
|
|
114
|
-
<p><strong>Matériel :</strong> ${escapeHtml(r.itemId)}</p>
|
|
115
|
-
<p><strong>Quantité :</strong> ${escapeHtml(r.quantity)}</p>
|
|
116
|
-
<p><strong>Dates :</strong> ${escapeHtml(r.dateStart)} → ${escapeHtml(r.dateEnd)} (${escapeHtml(r.days||'')} jours)</p>
|
|
117
|
-
<p><strong>Lieu :</strong> ${escapeHtml(r.pickupLocationName || r.pickupLocation || 'Non précisé')}</p>
|
|
118
|
-
<button class="btn btn-success btn-sm js-validate">Valider</button>
|
|
119
|
-
<button class="btn btn-danger btn-sm js-cancel">Annuler</button>
|
|
120
|
-
</div>
|
|
121
|
-
`).join('');
|
|
122
|
-
return `
|
|
123
|
-
<div class="card mb-3">
|
|
124
|
-
<div class="card-header">
|
|
125
|
-
<strong>${escapeHtml(ev.title)}</strong><br>
|
|
126
|
-
<small>${escapeHtml(ev.start)} → ${escapeHtml(ev.end)}</small>
|
|
127
|
-
</div>
|
|
128
|
-
<div class="card-body">${rows}</div>
|
|
129
|
-
</div>
|
|
130
|
-
`;
|
|
131
|
-
}).join('');
|
|
132
|
-
} catch (e) {
|
|
133
|
-
console.error('[calendar-onekite] loadPending error', e);
|
|
134
|
-
alerts.error(e?.message || e);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Planning graphical
|
|
139
|
-
async function loadInventory() {
|
|
140
|
-
const data = await api.get('/calendar/inventory');
|
|
141
|
-
STATE.locations = Array.isArray(data?.locations) ? data.locations : [];
|
|
142
|
-
STATE.inventory = Array.isArray(data?.inventory) ? data.inventory : [];
|
|
143
|
-
}
|
|
144
|
-
async function loadPlanningRows() {
|
|
145
|
-
STATE.rows = await api.get('/admin/calendar/planning');
|
|
146
|
-
if (!Array.isArray(STATE.rows)) STATE.rows = [];
|
|
147
|
-
}
|
|
148
|
-
function fillFilters() {
|
|
149
|
-
const locSel = qs('#planning-filter-location');
|
|
150
|
-
const itemSel = qs('#planning-filter-item');
|
|
151
|
-
if (!locSel || !itemSel) return;
|
|
152
|
-
locSel.innerHTML = '<option value="">Tous</option>' + STATE.locations.map(l =>
|
|
153
|
-
`<option value="${escapeHtml(l.id)}">${escapeHtml(l.name||l.id)}</option>`
|
|
154
|
-
).join('');
|
|
155
|
-
itemSel.innerHTML = '<option value="">Tous</option>' + STATE.inventory.map(i =>
|
|
156
|
-
`<option value="${escapeHtml(i.id)}">${escapeHtml(i.name||i.id)}</option>`
|
|
157
|
-
).join('');
|
|
158
|
-
}
|
|
159
|
-
function colorForStatus(status) {
|
|
160
|
-
if (status === 'paid') return '#d4edda';
|
|
161
|
-
if (status === 'awaiting_payment') return '#fff3cd';
|
|
162
|
-
if (status === 'pending_admin') return '#cfe2ff';
|
|
163
|
-
return '#e2e3e5';
|
|
164
|
-
}
|
|
165
|
-
function toFcEvents() {
|
|
166
|
-
const loc = qs('#planning-filter-location')?.value || '';
|
|
167
|
-
const item = qs('#planning-filter-item')?.value || '';
|
|
168
|
-
const status = qs('#planning-filter-status')?.value || '';
|
|
169
|
-
return STATE.rows
|
|
170
|
-
.filter(r => !loc || r.locationId === loc)
|
|
171
|
-
.filter(r => !item || r.itemId === item)
|
|
172
|
-
.filter(r => !status || r.status === status)
|
|
173
|
-
.map(r => {
|
|
174
|
-
const end = new Date(r.dateEnd);
|
|
175
|
-
end.setDate(end.getDate() + 1);
|
|
176
|
-
const c = colorForStatus(r.status);
|
|
177
|
-
return {
|
|
178
|
-
id: String(r.rid),
|
|
179
|
-
title: `${r.itemName} x${r.quantity} — ${r.pickupLocation} — uid ${r.uid}`,
|
|
180
|
-
start: r.dateStart,
|
|
181
|
-
end: end.toISOString().slice(0,10),
|
|
182
|
-
allDay: true,
|
|
183
|
-
backgroundColor: c,
|
|
184
|
-
borderColor: c,
|
|
185
|
-
extendedProps: r,
|
|
186
|
-
};
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
function renderPlanningCalendar() {
|
|
190
|
-
const el = qs('#planning-calendar');
|
|
191
|
-
if (!el) return;
|
|
192
|
-
const FC = window.FullCalendar;
|
|
193
|
-
if (!FC) return alerts.error('FullCalendar non chargé (CDN bloqué ?)');
|
|
194
|
-
|
|
195
|
-
if (STATE.fc) {
|
|
196
|
-
STATE.fc.removeAllEvents();
|
|
197
|
-
STATE.fc.addEventSource(toFcEvents());
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
STATE.fc = new FC.Calendar(el, {
|
|
201
|
-
initialView: 'timeGridWeek',
|
|
202
|
-
height: 'auto',
|
|
203
|
-
locale: 'fr',
|
|
204
|
-
firstDay: 1,
|
|
205
|
-
headerToolbar: { left: 'prev,next today', center: 'title', right: 'timeGridWeek,dayGridMonth,listWeek' },
|
|
206
|
-
events: toFcEvents(),
|
|
207
|
-
});
|
|
208
|
-
STATE.fc.render();
|
|
209
|
-
}
|
|
210
|
-
function bindPlanningFilters() {
|
|
211
|
-
['#planning-filter-location', '#planning-filter-item', '#planning-filter-status'].forEach(sel => {
|
|
212
|
-
qs(sel)?.addEventListener('change', renderPlanningCalendar);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function initPlanning() {
|
|
217
|
-
if (!qs('#planning-calendar')) return;
|
|
218
|
-
try {
|
|
219
|
-
await Promise.all([loadInventory(), loadPlanningRows()]);
|
|
220
|
-
fillFilters();
|
|
221
|
-
bindPlanningFilters();
|
|
222
|
-
renderPlanningCalendar();
|
|
223
|
-
} catch (e) {
|
|
224
|
-
console.error('[calendar-onekite] initPlanning error', e);
|
|
225
|
-
alerts.error(e?.message || e);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function init() {
|
|
230
|
-
bindAdminSettings();
|
|
231
|
-
initPlanning();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// NodeBB ajaxify navigation: re-init on page change
|
|
235
|
-
document.addEventListener('action:ajaxify.end', init);
|
|
236
|
-
onReady(init);
|
|
237
|
-
init();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Load NodeBB modules
|
|
241
|
-
if (typeof require === 'function') {
|
|
242
|
-
require(['api', 'alerts'], function (api, alerts) {
|
|
243
|
-
try { boot(api, alerts); } catch (e) { console.error(e); }
|
|
17
|
+
window.require([
|
|
18
|
+
'plugins/nodebb-plugin-calendar-onekite/static/js/admin'
|
|
19
|
+
], function (Admin) {
|
|
20
|
+
try { Admin && Admin.init && Admin.init(); } catch (e) { console.error(e); }
|
|
244
21
|
});
|
|
245
|
-
} else {
|
|
246
|
-
console.error('[calendar-onekite] require is not defined');
|
|
247
22
|
}
|
|
23
|
+
|
|
24
|
+
ready(boot);
|
|
25
|
+
document.addEventListener('action:ajaxify.end', boot);
|
|
248
26
|
})();
|
|
@@ -1,269 +1,63 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
|
-
if (window.__onekiteCalendarBundleLoaded) { return; }
|
|
4
|
-
window.__onekiteCalendarBundleLoaded = true;
|
|
5
3
|
|
|
6
|
-
function
|
|
7
|
-
function
|
|
8
|
-
function show(el){ if (el) el.style.display='block'; }
|
|
9
|
-
function hide(el){ if (el) el.style.display='none'; }
|
|
10
|
-
function escapeHtml(s){
|
|
11
|
-
return String(s ?? '')
|
|
12
|
-
.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>')
|
|
13
|
-
.replaceAll('"','"').replaceAll("'","'");
|
|
14
|
-
}
|
|
15
|
-
function pad(n){ return String(n).padStart(2,'0'); }
|
|
16
|
-
function toLocalInputValue(iso){
|
|
17
|
-
if (!iso) return '';
|
|
18
|
-
const d=new Date(iso);
|
|
19
|
-
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
20
|
-
}
|
|
21
|
-
function fromLocalInputValue(val){
|
|
22
|
-
if (!val) return null;
|
|
23
|
-
const d=new Date(val);
|
|
24
|
-
if (isNaN(d.getTime())) return null;
|
|
25
|
-
return d.toISOString();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function boot(api, alerts) {
|
|
29
|
-
const STATE = { inited:false, fc:null, currentEid:null, canCreate:false, canBook:false, locations:[], inventory:[] };
|
|
30
|
-
|
|
31
|
-
async function loadPermissions() {
|
|
32
|
-
try {
|
|
33
|
-
const [c,b]=await Promise.all([api.get('/calendar/permissions/create'), api.get('/calendar/permissions/book')]);
|
|
34
|
-
STATE.canCreate=!!c?.allow; STATE.canBook=!!b?.allow;
|
|
35
|
-
} catch { STATE.canCreate=false; STATE.canBook=false; }
|
|
36
|
-
}
|
|
37
|
-
async function loadInventory() {
|
|
38
|
-
try {
|
|
39
|
-
const data=await api.get('/calendar/inventory');
|
|
40
|
-
STATE.locations=Array.isArray(data?.locations)?data.locations:[];
|
|
41
|
-
STATE.inventory=Array.isArray(data?.inventory)?data.inventory:[];
|
|
42
|
-
} catch { STATE.locations=[]; STATE.inventory=[]; }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function bindModalClose() {
|
|
46
|
-
qsa('.calendar-close').forEach(btn=>{
|
|
47
|
-
if (btn.__bound) return;
|
|
48
|
-
btn.__bound=true;
|
|
49
|
-
btn.addEventListener('click',()=>hide(btn.closest('.calendar-modal')));
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function renderBookingChecklist(selectedIds) {
|
|
54
|
-
const root=qs('#booking-items');
|
|
55
|
-
if (!root) return;
|
|
56
|
-
const sel=new Set((selectedIds||[]).map(String));
|
|
57
|
-
if (!STATE.inventory.length) { root.innerHTML='<p class="text-muted">Aucun matériel (configurez l’inventaire dans l’admin).</p>'; return; }
|
|
58
|
-
root.innerHTML=STATE.inventory.map(item=>{
|
|
59
|
-
const checked=sel.has(String(item.id))?'checked':'';
|
|
60
|
-
return `<div class="form-check">
|
|
61
|
-
<label class="form-check-label">
|
|
62
|
-
<input class="form-check-input js-book-item" type="checkbox" value="${escapeHtml(item.id)}" ${checked}>
|
|
63
|
-
${escapeHtml(item.name||item.id)} — ${Number(item.price||0)} €/jour
|
|
64
|
-
</label>
|
|
65
|
-
</div>`;
|
|
66
|
-
}).join('');
|
|
67
|
-
}
|
|
68
|
-
function readSelectedItemIds() {
|
|
69
|
-
return qsa('.js-book-item').filter(el=>el.checked).map(el=>String(el.value));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function resetEventModal() {
|
|
73
|
-
STATE.currentEid=null;
|
|
74
|
-
qs('#event-modal-title').textContent='Nouvel événement';
|
|
75
|
-
qs('#event-title').value='';
|
|
76
|
-
qs('#event-description').value='';
|
|
77
|
-
qs('#event-start').value='';
|
|
78
|
-
qs('#event-end').value='';
|
|
79
|
-
qs('#event-allDay').checked=false;
|
|
80
|
-
qs('#event-location').value='';
|
|
81
|
-
qs('#event-bookingEnabled').checked=false;
|
|
82
|
-
renderBookingChecklist([]);
|
|
83
|
-
qs('#event-delete').style.display='none';
|
|
84
|
-
qs('#event-reserve').style.display=STATE.canBook?'inline-block':'none';
|
|
85
|
-
}
|
|
86
|
-
function fillEventModal(ev) {
|
|
87
|
-
STATE.currentEid=String(ev.eid);
|
|
88
|
-
qs('#event-modal-title').textContent='Éditer l’événement';
|
|
89
|
-
qs('#event-title').value=ev.title||'';
|
|
90
|
-
qs('#event-description').value=ev.description||'';
|
|
91
|
-
qs('#event-start').value=toLocalInputValue(ev.start);
|
|
92
|
-
qs('#event-end').value=toLocalInputValue(ev.end);
|
|
93
|
-
qs('#event-allDay').checked=!!Number(ev.allDay);
|
|
94
|
-
qs('#event-location').value=ev.location||'';
|
|
95
|
-
qs('#event-bookingEnabled').checked=!!Number(ev.bookingEnabled);
|
|
96
|
-
renderBookingChecklist(ev.bookingItemIds||[]);
|
|
97
|
-
qs('#event-delete').style.display=STATE.canCreate?'inline-block':'none';
|
|
98
|
-
qs('#event-reserve').style.display=STATE.canBook?'inline-block':'none';
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function openEventModalNew(prefill={}) {
|
|
102
|
-
if (!STATE.canCreate) return alerts.error('Vous n’êtes pas autorisé à créer des événements.');
|
|
103
|
-
resetEventModal();
|
|
104
|
-
if (prefill.start) qs('#event-start').value=toLocalInputValue(prefill.start);
|
|
105
|
-
if (prefill.end) qs('#event-end').value=toLocalInputValue(prefill.end);
|
|
106
|
-
show(qs('#calendar-event-modal'));
|
|
107
|
-
}
|
|
108
|
-
async function openEventModalByEid(eid) {
|
|
109
|
-
try {
|
|
110
|
-
const ev=await api.get(`/calendar/event/${encodeURIComponent(eid)}`);
|
|
111
|
-
fillEventModal(ev);
|
|
112
|
-
show(qs('#calendar-event-modal'));
|
|
113
|
-
} catch (e) { console.error(e); alerts.error(e?.message||e); }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function saveEvent() {
|
|
117
|
-
if (!STATE.canCreate) return;
|
|
118
|
-
const payload={
|
|
119
|
-
title: qs('#event-title').value||'',
|
|
120
|
-
description: qs('#event-description').value||'',
|
|
121
|
-
start: fromLocalInputValue(qs('#event-start').value)||new Date().toISOString(),
|
|
122
|
-
end: fromLocalInputValue(qs('#event-end').value)||fromLocalInputValue(qs('#event-start').value)||new Date().toISOString(),
|
|
123
|
-
allDay: qs('#event-allDay').checked,
|
|
124
|
-
location: qs('#event-location').value||'',
|
|
125
|
-
bookingEnabled: qs('#event-bookingEnabled').checked,
|
|
126
|
-
bookingItemIds: readSelectedItemIds(),
|
|
127
|
-
visibility:'public',
|
|
128
|
-
};
|
|
129
|
-
try {
|
|
130
|
-
if (STATE.currentEid) await api.put(`/calendar/event/${encodeURIComponent(STATE.currentEid)}`, payload);
|
|
131
|
-
else await api.post('/calendar/event', payload);
|
|
132
|
-
alerts.success('Enregistré');
|
|
133
|
-
hide(qs('#calendar-event-modal'));
|
|
134
|
-
STATE.fc?.refetchEvents();
|
|
135
|
-
} catch (e) { console.error(e); alerts.error(e?.message||e); }
|
|
136
|
-
}
|
|
137
|
-
async function deleteEvent() {
|
|
138
|
-
if (!STATE.currentEid || !STATE.canCreate) return;
|
|
139
|
-
if (!window.confirm('Supprimer cet événement ?')) return;
|
|
140
|
-
try {
|
|
141
|
-
await api.del(`/calendar/event/${encodeURIComponent(STATE.currentEid)}`);
|
|
142
|
-
alerts.success('Supprimé');
|
|
143
|
-
hide(qs('#calendar-event-modal'));
|
|
144
|
-
STATE.fc?.refetchEvents();
|
|
145
|
-
} catch (e) { console.error(e); alerts.error(e?.message||e); }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function renderLocationSelect() {
|
|
149
|
-
const sel=qs('#reserve-location');
|
|
150
|
-
if (!sel) return;
|
|
151
|
-
sel.innerHTML = STATE.locations.map(l=>`<option value="${escapeHtml(l.id)}">${escapeHtml(l.name||l.id)}</option>`).join('') || '<option value="">(aucun lieu)</option>';
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function openReserveModal(ev) {
|
|
155
|
-
qs('#reserve-title').textContent=`Réserver – ${ev.title||''}`;
|
|
156
|
-
renderLocationSelect();
|
|
157
|
-
const allowed=new Set((ev.bookingItemIds||[]).map(String));
|
|
158
|
-
const items=STATE.inventory.filter(i=>allowed.has(String(i.id)));
|
|
159
|
-
const reserveItems=qs('#reserve-items');
|
|
160
|
-
reserveItems.innerHTML = items.length ? items.map(it=>`
|
|
161
|
-
<div class="form-check">
|
|
162
|
-
<label class="form-check-label">
|
|
163
|
-
<input class="form-check-input" type="radio" name="reserveItem" value="${escapeHtml(it.id)}">
|
|
164
|
-
${escapeHtml(it.name||it.id)} (${Number(it.price||0)} €/jour)
|
|
165
|
-
</label>
|
|
166
|
-
</div>
|
|
167
|
-
`).join('') : '<p>Aucun matériel réservable pour cet événement.</p>';
|
|
168
|
-
|
|
169
|
-
const toDate=(d)=>`${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
|
|
170
|
-
qs('#reserve-start').value=toDate(new Date(ev.start));
|
|
171
|
-
qs('#reserve-end').value=toDate(new Date(ev.end));
|
|
172
|
-
show(qs('#calendar-reserve-modal'));
|
|
173
|
-
}
|
|
4
|
+
function log(...a){ try{ console.log('[calendar-onekite]', ...a);}catch{} }
|
|
5
|
+
function warn(...a){ try{ console.warn('[calendar-onekite]', ...a);}catch{} }
|
|
174
6
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const dateEnd = qs('#reserve-end')?.value;
|
|
180
|
-
const quantity = Number(qs('#reserve-quantity')?.value||1);
|
|
181
|
-
if (!STATE.currentEid) return alerts.error('Aucun événement sélectionné.');
|
|
182
|
-
if (!itemId) return alerts.error('Choisissez un matériel.');
|
|
183
|
-
if (!locationId) return alerts.error('Choisissez un lieu.');
|
|
184
|
-
try {
|
|
185
|
-
const res = await api.post(`/calendar/event/${encodeURIComponent(STATE.currentEid)}/book`, { itemId, locationId, dateStart, dateEnd, quantity });
|
|
186
|
-
alerts.success(res?.message||'Demande envoyée');
|
|
187
|
-
hide(qs('#calendar-reserve-modal')); hide(qs('#calendar-event-modal'));
|
|
188
|
-
} catch (e) { console.error(e); alerts.error(e?.message||e); }
|
|
189
|
-
}
|
|
7
|
+
function ready(fn){
|
|
8
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
|
9
|
+
else fn();
|
|
10
|
+
}
|
|
190
11
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const btnDel=qs('#event-delete'); if (btnDel && !btnDel.__bound){ btnDel.__bound=true; btnDel.addEventListener('click', deleteEvent); }
|
|
196
|
-
const btnRes=qs('#event-reserve'); if (btnRes && !btnRes.__bound){
|
|
197
|
-
btnRes.__bound=true;
|
|
198
|
-
btnRes.addEventListener('click', async ()=>{
|
|
199
|
-
if (!STATE.canBook) return alerts.error('Vous n’êtes pas autorisé à réserver.');
|
|
200
|
-
if (!STATE.currentEid) return;
|
|
201
|
-
try { const ev=await api.get(`/calendar/event/${encodeURIComponent(STATE.currentEid)}`); openReserveModal(ev); }
|
|
202
|
-
catch(e){ console.error(e); alerts.error(e?.message||e); }
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
const btnConf=qs('#reserve-confirm'); if (btnConf && !btnConf.__bound){ btnConf.__bound=true; btnConf.addEventListener('click', submitReservation); }
|
|
12
|
+
function runWithModules(mods, cb){
|
|
13
|
+
if (typeof window.require === 'function') {
|
|
14
|
+
window.require(mods, function(){ cb.apply(null, arguments); });
|
|
15
|
+
return true;
|
|
206
16
|
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
207
19
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
editable:STATE.canCreate,
|
|
222
|
-
events: async (info, success, failure)=>{
|
|
223
|
-
try{
|
|
224
|
-
const events=await api.get(`/calendar/events?start=${encodeURIComponent(info.start.toISOString())}&end=${encodeURIComponent(info.end.toISOString())}`);
|
|
225
|
-
success((events||[]).map(ev=>({ id:String(ev.eid), title:ev.title, start:ev.start, end:ev.end, allDay:!!Number(ev.allDay) })));
|
|
226
|
-
} catch(e){ failure(e); }
|
|
227
|
-
},
|
|
228
|
-
select:(sel)=>{
|
|
229
|
-
if (STATE.canCreate) return openEventModalNew({ start: sel.startStr, end: sel.endStr });
|
|
230
|
-
if (STATE.canBook) return alerts.info('Cliquez sur un événement puis "Réserver"');
|
|
231
|
-
},
|
|
232
|
-
eventClick:(arg)=>openEventModalByEid(arg.event.id),
|
|
233
|
-
});
|
|
234
|
-
STATE.fc.render();
|
|
235
|
-
}
|
|
20
|
+
async function fetchJson(path, method='GET', body){
|
|
21
|
+
const url = path.startsWith('/api/') || path.startsWith('/api/v3/') ? path : ('/api/v3' + path);
|
|
22
|
+
const headers = { 'Content-Type':'application/json' };
|
|
23
|
+
const csrf = (window.ajaxify && window.ajaxify.data && window.ajaxify.data.csrf_token) || null;
|
|
24
|
+
if (csrf) headers['x-csrf-token'] = csrf;
|
|
25
|
+
const res = await fetch(url, { method, headers, credentials:'same-origin', body: body ? JSON.stringify(body): undefined });
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
let msg = res.statusText;
|
|
28
|
+
try { const j = await res.json(); msg = j.error || j.message || msg; } catch {}
|
|
29
|
+
const e = new Error(msg); e.status=res.status; throw e;
|
|
30
|
+
}
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
236
33
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
34
|
+
function initCalendar(api, alerts){
|
|
35
|
+
// use the existing calendar module (AMD) if present; else minimal inline init
|
|
36
|
+
if (runWithModules(['plugins/nodebb-plugin-calendar-onekite/static/js/calendar'], function(Calendar){
|
|
37
|
+
if (Calendar && Calendar.init) Calendar.init();
|
|
38
|
+
})) return;
|
|
241
39
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
40
|
+
// Fallback: we don't have AMD loader, do nothing but tell why
|
|
41
|
+
warn('AMD require introuvable, impossible d\'initialiser le calendrier.');
|
|
42
|
+
if (alerts && alerts.error) alerts.error('Calendrier: require introuvable (scripts NodeBB non chargés)');
|
|
43
|
+
}
|
|
245
44
|
|
|
246
|
-
|
|
247
|
-
|
|
45
|
+
function boot(){
|
|
46
|
+
if (!document.querySelector('#calendar')) return;
|
|
47
|
+
// Ensure FullCalendar loaded
|
|
48
|
+
if (!window.FullCalendar) {
|
|
49
|
+
warn('FullCalendar non chargé (CDN bloqué ?).');
|
|
248
50
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
51
|
+
runWithModules(['api', 'alerts'], function(api, alerts){
|
|
52
|
+
initCalendar(api, alerts);
|
|
53
|
+
}) || initCalendar(null, null);
|
|
252
54
|
}
|
|
253
55
|
|
|
254
|
-
|
|
255
|
-
qsa('.calendar-close').forEach(btn=>{
|
|
256
|
-
if (btn.__bound) return;
|
|
257
|
-
btn.__bound=true;
|
|
258
|
-
btn.addEventListener('click',()=>hide(btn.closest('.calendar-modal')));
|
|
259
|
-
});
|
|
260
|
-
}
|
|
56
|
+
ready(boot);
|
|
261
57
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
});
|
|
266
|
-
} else {
|
|
267
|
-
console.error('[calendar-onekite] require is not defined');
|
|
58
|
+
// NodeBB ajaxify navigation
|
|
59
|
+
if (window.socket && window.socket.on) {
|
|
60
|
+
// do nothing
|
|
268
61
|
}
|
|
62
|
+
document.addEventListener('action:ajaxify.end', boot);
|
|
269
63
|
})();
|
|
@@ -28,10 +28,6 @@
|
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
|
|
31
|
-
<script>
|
|
32
|
-
require(['plugins/nodebb-plugin-calendar-onekite/static/js/admin-planning'], function (Mod) {
|
|
33
|
-
Mod.init && Mod.init();
|
|
34
|
-
});
|
|
35
|
-
</script>
|
|
36
|
-
|
|
37
31
|
<script src="/plugins/nodebb-plugin-calendar-onekite/static/js/admin.bundle.js"></script>
|
|
32
|
+
|
|
33
|
+
<script src="/plugins/nodebb-plugin-calendar-onekite/static/js/admin-planning.bundle.js"></script>
|
|
@@ -74,8 +74,5 @@
|
|
|
74
74
|
<button id="calendar-onekite-save" class="btn btn-primary">Enregistrer</button>
|
|
75
75
|
</div>
|
|
76
76
|
|
|
77
|
-
<script>
|
|
78
|
-
require(['plugins/nodebb-plugin-calendar-onekite/static/js/admin'], function () {});
|
|
79
|
-
</script>
|
|
80
77
|
|
|
81
78
|
<script src="/plugins/nodebb-plugin-calendar-onekite/static/js/admin.bundle.js"></script>
|
package/templates/calendar.tpl
CHANGED
|
@@ -99,11 +99,6 @@
|
|
|
99
99
|
|
|
100
100
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
|
|
101
101
|
|
|
102
|
-
<script>
|
|
103
|
-
require(['plugins/nodebb-plugin-calendar-onekite/static/js/calendar'], function (Calendar) {
|
|
104
|
-
Calendar.init && Calendar.init();
|
|
105
|
-
});
|
|
106
|
-
</script>
|
|
107
102
|
<script src="/plugins/nodebb-plugin-calendar-onekite/static/js/calendar.bundle.js"></script>
|
|
108
103
|
|
|
109
104
|
<!-- IMPORT partials/footer.tpl -->
|