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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Calendar + equipment booking + admin approval + HelloAsso payments for NodeBB v4 (no-jQuery UI)",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
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.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 qs(sel, root) { return (root || document).querySelector(sel); }
7
- function qsa(sel, root) { return Array.from((root || document).querySelectorAll(sel)); }
8
- function escapeHtml(s) {
9
- return String(s ?? '')
10
- .replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
11
- .replaceAll('"','&quot;').replaceAll("'","&#039;");
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(api, alerts) {
20
- const STATE = { bound: false, fc: null, locations: [], inventory: [], rows: [] };
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
- const settings = {
30
- allowedGroups: qs('#calendar-onekite-groups')?.value || 'administrators',
31
- allowedBookingGroups: qs('#calendar-onekite-book-groups')?.value || 'registered-users',
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
- function bindAdminSettings() {
53
- const root = qs('#calendar-onekite-admin');
54
- if (!root) return;
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 qs(sel, root) { return (root || document).querySelector(sel); }
7
- function qsa(sel, root) { return Array.from((root || document).querySelectorAll(sel)); }
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('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
13
- .replaceAll('"','&quot;').replaceAll("'","&#039;");
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
- async function submitReservation() {
176
- const itemId = qs('input[name="reserveItem"]:checked')?.value;
177
- const locationId = qs('#reserve-location')?.value;
178
- const dateStart = qs('#reserve-start')?.value;
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
- function bindButtons() {
192
- const btnNew=qs('#calendar-new-event');
193
- if (btnNew && !btnNew.__bound) { btnNew.__bound=true; btnNew.addEventListener('click', ()=>openEventModalNew()); }
194
- const btnSave=qs('#event-save'); if (btnSave && !btnSave.__bound){ btnSave.__bound=true; btnSave.addEventListener('click', saveEvent); }
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
- function buildCalendar() {
209
- const el=qs('#calendar');
210
- if (!el) return;
211
- const FC=window.FullCalendar;
212
- if (!FC) return alerts.error('FullCalendar non chargé (CDN bloqué ?)');
213
- STATE.fc = new FC.Calendar(el, {
214
- initialView:'dayGridMonth',
215
- height:'auto',
216
- locale:'fr',
217
- firstDay:1,
218
- headerToolbar:{ left:'prev,next today', center:'title', right:'dayGridMonth,timeGridWeek,timeGridDay,listWeek' },
219
- selectable:(STATE.canCreate||STATE.canBook),
220
- selectMirror:true,
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
- async function init() {
238
- if (!qs('#calendar')) return;
239
- bindModalClose();
240
- bindButtons();
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
- await Promise.all([loadPermissions(), loadInventory()]);
243
- const btn=qs('#calendar-new-event');
244
- if (btn) btn.style.display = STATE.canCreate ? 'inline-block' : 'none';
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
- renderBookingChecklist([]);
247
- if (!STATE.fc) buildCalendar();
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
- document.addEventListener('action:ajaxify.end', init);
251
- init();
51
+ runWithModules(['api', 'alerts'], function(api, alerts){
52
+ initCalendar(api, alerts);
53
+ }) || initCalendar(null, null);
252
54
  }
253
55
 
254
- function bindModalClose() {
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
- if (typeof require === 'function') {
263
- require(['api','alerts'], function(api, alerts){
264
- try { boot(api, alerts); } catch(e){ console.error(e); }
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>
@@ -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 -->