nodebb-plugin-calendar-onekite 2.0.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.
@@ -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
+ })();
@@ -8,6 +8,9 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
8
8
  currentEid: null,
9
9
  canCreate: false,
10
10
  canBook: false,
11
+ locations: [],
12
+ inventory: [],
13
+ currentEvent: null,
11
14
  };
12
15
 
13
16
  function qs(sel, root = document) { return root.querySelector(sel); }
@@ -51,6 +54,17 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
51
54
  }
52
55
  }
53
56
 
57
+ async function loadInventory() {
58
+ try {
59
+ const data = await api.get('/calendar/inventory');
60
+ STATE.locations = Array.isArray(data?.locations) ? data.locations : [];
61
+ STATE.inventory = Array.isArray(data?.inventory) ? data.inventory : [];
62
+ } catch (e) {
63
+ STATE.locations = [];
64
+ STATE.inventory = [];
65
+ }
66
+ }
67
+
54
68
  function bindModalClose() {
55
69
  qsa('.calendar-close').forEach(btn => {
56
70
  btn.addEventListener('click', () => hide(btn.closest('.calendar-modal')));
@@ -59,6 +73,7 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
59
73
 
60
74
  function resetEventModal() {
61
75
  STATE.currentEid = null;
76
+ STATE.currentEvent = null;
62
77
  qs('#event-modal-title').textContent = 'Nouvel événement';
63
78
  qs('#event-title').value = '';
64
79
  qs('#event-description').value = '';
@@ -67,49 +82,46 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
67
82
  qs('#event-allDay').checked = false;
68
83
  qs('#event-location').value = '';
69
84
  qs('#event-bookingEnabled').checked = false;
70
- qs('#booking-items').innerHTML = '<p>Aucun matériel.</p>';
85
+
86
+ renderBookingChecklist([]);
71
87
 
72
88
  qs('#event-delete').style.display = 'none';
73
89
  qs('#event-reserve').style.display = STATE.canBook ? 'inline-block' : 'none';
74
90
  }
75
91
 
76
- function renderBookingItems(items) {
92
+ function renderBookingChecklist(selectedIds) {
77
93
  const root = qs('#booking-items');
78
94
  if (!root) return;
79
- if (!Array.isArray(items) || items.length === 0) {
80
- root.innerHTML = '<p>Aucun matériel.</p>';
95
+
96
+ const sel = new Set((selectedIds || []).map(String));
97
+ if (!STATE.inventory.length) {
98
+ root.innerHTML = '<p class="text-muted">Aucun matériel (configurez l’inventaire dans l’admin).</p>';
81
99
  return;
82
100
  }
83
- root.innerHTML = items.map((it, idx) => `
84
- <div class="onekite-item" data-idx="${idx}">
85
- <div class="form-group"><label>Nom</label><input class="form-control js-item-name" value="${escapeHtml(it.name||'')}"></div>
86
- <div class="form-group"><label>ID</label><input class="form-control js-item-id" value="${escapeHtml(it.id||'')}"></div>
87
- <div class="form-group"><label>Total dispo</label><input type="number" class="form-control js-item-total" value="${Number(it.total||0)}" min="0"></div>
88
- <div class="form-group"><label>Prix / jour</label><input type="number" class="form-control js-item-price" value="${Number(it.price||0)}" min="0"></div>
89
- <div class="form-group"><label>Lieu retrait</label><input class="form-control js-item-pickup" value="${escapeHtml(it.pickupLocation||'')}"></div>
90
- <button class="btn btn-sm btn-danger js-item-remove">Supprimer</button>
91
- <hr>
92
- </div>
93
- `).join('');
101
+
102
+ root.innerHTML = STATE.inventory.map(item => {
103
+ const checked = sel.has(String(item.id)) ? 'checked' : '';
104
+ return `
105
+ <div class="form-check">
106
+ <label class="form-check-label">
107
+ <input class="form-check-input js-book-item" type="checkbox" value="${escapeHtml(item.id)}" ${checked}>
108
+ ${escapeHtml(item.name || item.id)} ${Number(item.price||0)} €/jour
109
+ </label>
110
+ </div>
111
+ `;
112
+ }).join('');
94
113
  }
95
114
 
96
- function readBookingItemsFromModal() {
97
- const blocks = qsa('#booking-items .onekite-item');
98
- const items = [];
99
- for (const b of blocks) {
100
- const name = b.querySelector('.js-item-name')?.value?.trim() || '';
101
- const id = b.querySelector('.js-item-id')?.value?.trim() || '';
102
- const total = Number(b.querySelector('.js-item-total')?.value || 0);
103
- const price = Number(b.querySelector('.js-item-price')?.value || 0);
104
- const pickupLocation = b.querySelector('.js-item-pickup')?.value?.trim() || '';
105
- if (!id) continue;
106
- items.push({ id, name, total, price, pickupLocation });
107
- }
108
- return items;
115
+ function readSelectedItemIds() {
116
+ return qsa('.js-book-item')
117
+ .filter(el => el.checked)
118
+ .map(el => String(el.value));
109
119
  }
110
120
 
111
121
  function fillEventModal(ev) {
112
122
  STATE.currentEid = String(ev.eid);
123
+ STATE.currentEvent = ev;
124
+
113
125
  qs('#event-modal-title').textContent = 'Éditer l’événement';
114
126
  qs('#event-title').value = ev.title || '';
115
127
  qs('#event-description').value = ev.description || '';
@@ -119,7 +131,8 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
119
131
  qs('#event-location').value = ev.location || '';
120
132
  qs('#event-bookingEnabled').checked = !!Number(ev.bookingEnabled);
121
133
 
122
- renderBookingItems(ev.bookingItems || []);
134
+ const ids = Array.isArray(ev.bookingItemIds) ? ev.bookingItemIds : (Array.isArray(ev.bookingItems) ? ev.bookingItems.map(x => x.id) : []);
135
+ renderBookingChecklist(ids);
123
136
 
124
137
  qs('#event-delete').style.display = STATE.canCreate ? 'inline-block' : 'none';
125
138
  qs('#event-reserve').style.display = STATE.canBook ? 'inline-block' : 'none';
@@ -154,7 +167,7 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
154
167
  allDay: qs('#event-allDay').checked,
155
168
  location: qs('#event-location').value || '',
156
169
  bookingEnabled: qs('#event-bookingEnabled').checked,
157
- bookingItems: readBookingItemsFromModal(),
170
+ bookingItemIds: readSelectedItemIds(),
158
171
  visibility: 'public',
159
172
  };
160
173
 
@@ -184,107 +197,70 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
184
197
  hide(qs('#calendar-event-modal'));
185
198
  STATE.fc?.refetchEvents();
186
199
  } catch (e) {
187
- // Some NodeBB api wrappers don't have del(); fallback to fetch
188
- try {
189
- const csrf = window.config?.csrf_token || window.config?.csrfToken;
190
- const res = await fetch(`/api/v3/calendar/event/${encodeURIComponent(STATE.currentEid)}`, {
191
- method: 'DELETE',
192
- headers: csrf ? { 'x-csrf-token': csrf } : {},
193
- credentials: 'same-origin',
194
- });
195
- if (!res.ok) throw new Error(await res.text());
196
- alerts.success('Événement supprimé');
197
- hide(qs('#calendar-event-modal'));
198
- STATE.fc?.refetchEvents();
199
- } catch (e2) {
200
- alerts.error(e2?.message || e2);
201
- }
200
+ alerts.error(e?.message || e);
202
201
  }
203
202
  }
204
203
 
205
- function addBookingItemRow() {
206
- const root = qs('#booking-items');
207
- if (!root) return;
208
- if (root.textContent.includes('Aucun matériel')) root.innerHTML = '';
209
-
210
- root.insertAdjacentHTML('beforeend', `
211
- <div class="onekite-item">
212
- <div class="form-group"><label>Nom</label><input class="form-control js-item-name" value=""></div>
213
- <div class="form-group"><label>ID</label><input class="form-control js-item-id" value=""></div>
214
- <div class="form-group"><label>Total dispo</label><input type="number" class="form-control js-item-total" value="0" min="0"></div>
215
- <div class="form-group"><label>Prix / jour</label><input type="number" class="form-control js-item-price" value="0" min="0"></div>
216
- <div class="form-group"><label>Lieu retrait</label><input class="form-control js-item-pickup" value=""></div>
217
- <button class="btn btn-sm btn-danger js-item-remove">Supprimer</button>
218
- <hr>
219
- </div>
220
- `);
221
- }
204
+ /* ---------- Reservation UI ---------- */
222
205
 
223
- function bindEventModalButtons() {
224
- qs('#event-save')?.addEventListener('click', saveEvent);
225
- qs('#event-delete')?.addEventListener('click', deleteEvent);
226
- qs('#event-add-item')?.addEventListener('click', addBookingItemRow);
206
+ function renderLocationSelect(selected) {
207
+ const sel = qs('#reserve-location');
208
+ if (!sel) return;
209
+ const opts = STATE.locations.map(l => `<option value="${escapeHtml(l.id)}">${escapeHtml(l.name||l.id)}</option>`).join('');
210
+ sel.innerHTML = opts || '<option value="">(aucun lieu)</option>';
211
+ if (selected) sel.value = selected;
212
+ }
227
213
 
228
- qs('#booking-items')?.addEventListener('click', (ev) => {
229
- const t = ev.target;
230
- if (!(t instanceof HTMLElement)) return;
231
- if (t.classList.contains('js-item-remove')) {
232
- t.closest('.onekite-item')?.remove();
233
- }
234
- });
214
+ function openReserveModal(event, prefillDates) {
215
+ qs('#reserve-title').textContent = `Réserver – ${event.title || ''}`;
235
216
 
236
- // Reservation flow: open reserve modal and fill items
237
- qs('#event-reserve')?.addEventListener('click', async () => {
238
- if (!STATE.canBook) return alerts.error('Vous n’êtes pas autorisé à réserver.');
239
- if (!STATE.currentEid) return;
217
+ renderLocationSelect(STATE.locations[0]?.id || '');
240
218
 
241
- try {
242
- const ev = await api.get(`/calendar/event/${encodeURIComponent(STATE.currentEid)}`);
243
- openReserveModal(ev);
244
- } catch (e) {
245
- alerts.error(e?.message || e);
246
- }
247
- });
248
- }
219
+ const allowed = new Set((event.bookingItemIds || []).map(String));
220
+ const items = STATE.inventory.filter(i => allowed.has(String(i.id)));
249
221
 
250
- function openReserveModal(event) {
251
- // Fill title
252
- qs('#reserve-title').textContent = `Réserver – ${event.title || ''}`;
253
- const items = Array.isArray(event.bookingItems) ? event.bookingItems : [];
254
222
  const reserveItems = qs('#reserve-items');
255
223
  reserveItems.innerHTML = items.length ? items.map(it => `
256
224
  <div class="form-check">
257
225
  <label class="form-check-label">
258
226
  <input class="form-check-input js-reserve-item" type="radio" name="reserveItem" value="${escapeHtml(it.id)}">
259
- ${escapeHtml(it.name || it.id)} (dispo: ${Number(it.total||0)}, ${Number(it.price||0)} €/jour) — retrait: ${escapeHtml(it.pickupLocation||'')}
227
+ ${escapeHtml(it.name || it.id)} (${Number(it.price||0)} €/jour)
260
228
  </label>
261
229
  </div>
262
- `).join('') : '<p>Aucun matériel réservable.</p>';
230
+ `).join('') : '<p>Aucun matériel réservable pour cet événement.</p>';
263
231
 
264
- // Pre-fill dates from event
265
- const start = new Date(event.start);
266
- const end = new Date(event.end);
267
232
  const toDateInput = (d) => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
268
- qs('#reserve-start').value = toDateInput(start);
269
- qs('#reserve-end').value = toDateInput(end);
233
+
234
+ if (prefillDates?.dateStart && prefillDates?.dateEnd) {
235
+ qs('#reserve-start').value = prefillDates.dateStart;
236
+ qs('#reserve-end').value = prefillDates.dateEnd;
237
+ } else {
238
+ const start = new Date(event.start);
239
+ const end = new Date(event.end);
240
+ qs('#reserve-start').value = toDateInput(start);
241
+ qs('#reserve-end').value = toDateInput(end);
242
+ }
270
243
 
271
244
  show(qs('#calendar-reserve-modal'));
272
245
  }
273
246
 
274
247
  async function submitReservationFromModal() {
275
248
  const itemId = qs('input[name="reserveItem"]:checked')?.value;
249
+ const locationId = qs('#reserve-location')?.value;
276
250
  const dateStart = qs('#reserve-start')?.value;
277
251
  const dateEnd = qs('#reserve-end')?.value;
278
252
  const quantity = Number(qs('#reserve-quantity')?.value || 1);
279
253
 
280
254
  if (!STATE.currentEid) return alerts.error('Aucun événement sélectionné.');
281
255
  if (!itemId) return alerts.error('Choisissez un matériel.');
256
+ if (!locationId) return alerts.error('Choisissez un lieu.');
282
257
  if (!dateStart || !dateEnd) return alerts.error('Choisissez des dates.');
283
258
  if (quantity <= 0) return alerts.error('Quantité invalide.');
284
259
 
285
260
  try {
286
261
  const res = await api.post(`/calendar/event/${encodeURIComponent(STATE.currentEid)}/book`, {
287
262
  itemId,
263
+ locationId,
288
264
  quantity,
289
265
  dateStart,
290
266
  dateEnd,
@@ -292,16 +268,47 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
292
268
  alerts.success(res?.message || 'Demande envoyée.');
293
269
  hide(qs('#calendar-reserve-modal'));
294
270
  hide(qs('#calendar-event-modal'));
295
- STATE.fc?.refetchEvents();
296
271
  } catch (e) {
297
272
  alerts.error(e?.message || e);
298
273
  }
299
274
  }
300
275
 
276
+ function bindEventModalButtons() {
277
+ qs('#event-save')?.addEventListener('click', saveEvent);
278
+ qs('#event-delete')?.addEventListener('click', deleteEvent);
279
+
280
+ // Reservation flow: open reserve modal from event
281
+ qs('#event-reserve')?.addEventListener('click', async () => {
282
+ if (!STATE.canBook) return alerts.error('Vous n’êtes pas autorisé à réserver.');
283
+ if (!STATE.currentEid) return;
284
+
285
+ try {
286
+ const ev = await api.get(`/calendar/event/${encodeURIComponent(STATE.currentEid)}`);
287
+ openReserveModal(ev);
288
+ } catch (e) {
289
+ alerts.error(e?.message || e);
290
+ }
291
+ });
292
+ }
293
+
301
294
  function bindReserveModalButtons() {
295
+ // Add location select if missing (back-compat)
296
+ if (!qs('#reserve-location')) {
297
+ const itemsRoot = qs('#reserve-items');
298
+ if (itemsRoot) {
299
+ itemsRoot.insertAdjacentHTML('beforebegin', `
300
+ <div class="form-group">
301
+ <label>Lieu de retrait</label>
302
+ <select id="reserve-location" class="form-control"></select>
303
+ </div>
304
+ `);
305
+ }
306
+ }
302
307
  qs('#reserve-confirm')?.addEventListener('click', submitReservationFromModal);
303
308
  }
304
309
 
310
+ /* ---------- FullCalendar ---------- */
311
+
305
312
  function buildCalendar() {
306
313
  const el = qs('#calendar');
307
314
  if (!el) return;
@@ -319,7 +326,7 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
319
326
  center: 'title',
320
327
  right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek',
321
328
  },
322
- selectable: STATE.canCreate,
329
+ selectable: (STATE.canCreate || STATE.canBook),
323
330
  selectMirror: true,
324
331
  editable: STATE.canCreate,
325
332
  eventDurationEditable: STATE.canCreate,
@@ -343,8 +350,22 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
343
350
  }
344
351
  },
345
352
 
346
- select: (sel) => {
347
- openEventModalNew({ start: sel.startStr, end: sel.endStr });
353
+ // C mode: selection can create OR open reservation helper
354
+ select: async (sel) => {
355
+ const dateStart = sel.startStr.slice(0,10);
356
+ // FullCalendar end is exclusive; show end-1 day for date inputs
357
+ const endDate = new Date(sel.end);
358
+ endDate.setDate(endDate.getDate() - 1);
359
+ const dateEnd = `${endDate.getFullYear()}-${pad(endDate.getMonth()+1)}-${pad(endDate.getDate())}`;
360
+
361
+ if (STATE.canCreate) {
362
+ openEventModalNew({ start: sel.startStr, end: sel.endStr });
363
+ return;
364
+ }
365
+ if (STATE.canBook) {
366
+ alerts.info('Cliquez sur un événement, puis "Réserver" pour lier la réservation à un événement.');
367
+ return;
368
+ }
348
369
  },
349
370
 
350
371
  eventClick: (arg) => {
@@ -400,11 +421,14 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
400
421
  bindEventModalButtons();
401
422
  bindReserveModalButtons();
402
423
 
403
- loadPermissions().then(() => {
404
- const btn = qs('#calendar-new-event');
405
- if (btn) btn.style.display = STATE.canCreate ? 'inline-block' : 'none';
406
- buildCalendar();
407
- });
424
+ Promise.all([loadPermissions(), loadInventory()])
425
+ .then(() => {
426
+ const btn = qs('#calendar-new-event');
427
+ if (btn) btn.style.display = STATE.canCreate ? 'inline-block' : 'none';
428
+ // default inventory checklist for new event
429
+ renderBookingChecklist([]);
430
+ buildCalendar();
431
+ });
408
432
  }
409
433
 
410
434
  function resetIfLeft() {
@@ -412,6 +436,7 @@ define('plugins/nodebb-plugin-calendar-onekite/static/js/calendar', ['api', 'ale
412
436
  STATE.inited = false;
413
437
  STATE.fc = null;
414
438
  STATE.currentEid = null;
439
+ STATE.currentEvent = null;
415
440
  }
416
441
  }
417
442