nodebb-plugin-calendar-onekite 2.1.0 → 2.1.1

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.0",
3
+ "version": "2.1.1",
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,13 @@
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.0",
6
+ "version": "2.1.1",
7
7
  "library": "./library.js",
8
8
  "staticDirs": {
9
9
  "static": "static"
10
10
  },
11
11
  "acpScripts": [
12
- "static/js/admin.js"
12
+ "static/js/admin.bundle.js"
13
13
  ],
14
14
  "hooks": [
15
15
  {
@@ -0,0 +1,248 @@
1
+ (function () {
2
+ 'use strict';
3
+ if (window.__onekiteAdminBundleLoaded) { return; }
4
+ window.__onekiteAdminBundleLoaded = true;
5
+
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 });
16
+ else fn();
17
+ }
18
+
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'); }
28
+
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
+ }
50
+ }
51
+
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); }
244
+ });
245
+ } else {
246
+ console.error('[calendar-onekite] require is not defined');
247
+ }
248
+ })();
@@ -0,0 +1,269 @@
1
+ (function () {
2
+ 'use strict';
3
+ if (window.__onekiteCalendarBundleLoaded) { return; }
4
+ window.__onekiteCalendarBundleLoaded = true;
5
+
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
+ }
174
+
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
+ }
190
+
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); }
206
+ }
207
+
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
+ }
236
+
237
+ async function init() {
238
+ if (!qs('#calendar')) return;
239
+ bindModalClose();
240
+ bindButtons();
241
+
242
+ await Promise.all([loadPermissions(), loadInventory()]);
243
+ const btn=qs('#calendar-new-event');
244
+ if (btn) btn.style.display = STATE.canCreate ? 'inline-block' : 'none';
245
+
246
+ renderBookingChecklist([]);
247
+ if (!STATE.fc) buildCalendar();
248
+ }
249
+
250
+ document.addEventListener('action:ajaxify.end', init);
251
+ init();
252
+ }
253
+
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
+ }
261
+
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');
268
+ }
269
+ })();
@@ -0,0 +1,42 @@
1
+ (function(){
2
+ 'use strict';
3
+ if (window.__onekiteMyResLoaded) return;
4
+ window.__onekiteMyResLoaded=true;
5
+
6
+ function qs(sel, root){ return (root||document).querySelector(sel); }
7
+ function escapeHtml(s){
8
+ return String(s ?? '').replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
9
+ .replaceAll('"','&quot;').replaceAll("'","&#039;");
10
+ }
11
+
12
+ function boot(api, alerts){
13
+ async function load(){
14
+ const tbody=qs('#my-reservations-body');
15
+ if (!tbody) return;
16
+ try{
17
+ const rows=await api.get('/calendar/my-reservations');
18
+ if (!rows || !rows.length){
19
+ tbody.innerHTML='<tr><td colspan="7">Aucune réservation.</td></tr>'; return;
20
+ }
21
+ tbody.innerHTML = rows.map(r=>`
22
+ <tr>
23
+ <td>${escapeHtml(r.eventTitle)}</td>
24
+ <td>${escapeHtml(r.itemName)}</td>
25
+ <td>${escapeHtml(r.pickupLocation || '')}</td>
26
+ <td>${escapeHtml(r.dateStart)}</td>
27
+ <td>${escapeHtml(r.dateEnd)}</td>
28
+ <td>${escapeHtml(r.days || '')}</td>
29
+ <td>${escapeHtml(r.status)}</td>
30
+ </tr>
31
+ `).join('');
32
+ } catch(e){ console.error(e); alerts.error(e?.message||e); }
33
+ }
34
+ async function init(){ if (!qs('#calendar-my-reservations')) return; await load(); }
35
+ document.addEventListener('action:ajaxify.end', init);
36
+ init();
37
+ }
38
+
39
+ if (typeof require === 'function'){
40
+ require(['api','alerts'], function(api, alerts){ boot(api, alerts); });
41
+ }
42
+ })();
@@ -33,3 +33,5 @@ require(['plugins/nodebb-plugin-calendar-onekite/static/js/admin-planning'], fun
33
33
  Mod.init && Mod.init();
34
34
  });
35
35
  </script>
36
+
37
+ <script src="/plugins/nodebb-plugin-calendar-onekite/static/js/admin.bundle.js"></script>
@@ -77,3 +77,5 @@
77
77
  <script>
78
78
  require(['plugins/nodebb-plugin-calendar-onekite/static/js/admin'], function () {});
79
79
  </script>
80
+
81
+ <script src="/plugins/nodebb-plugin-calendar-onekite/static/js/admin.bundle.js"></script>
@@ -26,4 +26,6 @@ require(['plugins/nodebb-plugin-calendar-onekite/static/js/calendar-my-reservati
26
26
  });
27
27
  </script>
28
28
 
29
+ <script src="/plugins/nodebb-plugin-calendar-onekite/static/js/my-reservations.bundle.js"></script>
30
+
29
31
  <!-- IMPORT partials/footer.tpl -->
@@ -104,5 +104,6 @@ require(['plugins/nodebb-plugin-calendar-onekite/static/js/calendar'], function
104
104
  Calendar.init && Calendar.init();
105
105
  });
106
106
  </script>
107
+ <script src="/plugins/nodebb-plugin-calendar-onekite/static/js/calendar.bundle.js"></script>
107
108
 
108
109
  <!-- IMPORT partials/footer.tpl -->