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.
- package/README.md +7 -0
- package/library.js +230 -148
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/static/js/admin-planning.js +133 -0
- package/static/js/admin.bundle.js +248 -0
- package/static/js/admin.js +9 -0
- package/static/js/calendar.bundle.js +269 -0
- package/static/js/calendar.js +130 -105
- package/static/js/my-reservations.bundle.js +42 -0
- package/templates/admin/calendar-planning.tpl +32 -20
- package/templates/admin/plugins/calendar-onekite.tpl +20 -0
- package/templates/calendar-my-reservations.tpl +2 -0
- package/templates/calendar.tpl +11 -2
|
@@ -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('&','&').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
|
+
}
|
|
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
|
+
})();
|
package/static/js/calendar.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
92
|
+
function renderBookingChecklist(selectedIds) {
|
|
77
93
|
const root = qs('#booking-items');
|
|
78
94
|
if (!root) return;
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<div class="form-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
224
|
-
qs('#
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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)} (
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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()
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|