nodebb-plugin-equipment-calendar 0.2.7 → 0.2.9

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/library.js CHANGED
@@ -313,6 +313,7 @@ plugin.init = async function (params) {
313
313
  if (mid && mid.admin) {
314
314
  router.get('/admin/plugins/equipment-calendar', mid.admin.buildHeader, renderAdminPage);
315
315
  router.get('/api/admin/plugins/equipment-calendar', renderAdminPage);
316
+ router.post('/admin/plugins/equipment-calendar/save', middleware.applyCSRF, handleAdminSave);
316
317
  }
317
318
 
318
319
  // Convenience alias (optional): /calendar -> /equipment/calendar
@@ -413,14 +414,8 @@ async function renderCalendarPage(req, res) {
413
414
  const settings = await getSettings();
414
415
  const items = parseItems(settings.itemsJson).filter(i => i.active);
415
416
 
416
- const rawItems = items;
417
-
418
417
  const tz = settings.timezone || 'Europe/Paris';
419
418
 
420
- const itemId = String(req.query.itemId || (rawItems[0]?.id || '')).trim();
421
- const chosenItem = rawItems.find(i => i.id === itemId) || rawItems[0] || null;
422
- const itemsView = rawItems.map(it => ({ ...it, selected: chosenItem ? it.id === chosenItem.id : false }));
423
-
424
419
  // Determine range to render
425
420
  const now = DateTime.now().setZone(tz);
426
421
  const view = String(req.query.view || settings.defaultView || 'dayGridMonth');
@@ -438,43 +433,66 @@ async function renderCalendarPage(req, res) {
438
433
  end = now.endOf('month').plus({ days: 1 });
439
434
  }
440
435
 
441
- // Load reservations for chosen item within range
442
- const reservations = chosenItem ? await listReservationsForRange(chosenItem.id, start.toMillis(), end.toMillis()) : [];
436
+ // Load reservations for ALL items within range (so we can build availability client-side without extra requests)
437
+ const allReservations = [];
438
+ for (const it of items) {
439
+ // eslint-disable-next-line no-await-in-loop
440
+ const resForItem = await listReservationsForRange(it.id, start.toMillis(), end.toMillis());
441
+ for (const r of resForItem) allReservations.push(r);
442
+ }
443
+
443
444
  const showRequesterToAll = String(settings.showRequesterToAll) === '1';
444
445
  const isApprover = req.uid ? await canApprove(req.uid, settings) : false;
445
446
 
446
- const requesterUids = Array.from(new Set(reservations.map(r => r.uid))).filter(Boolean);
447
+ const requesterUids = Array.from(new Set(allReservations.map(r => r.uid))).filter(Boolean);
447
448
  const users = requesterUids.length ? await user.getUsersData(requesterUids) : [];
448
449
  const nameByUid = {};
449
450
  (users || []).forEach(u => { nameByUid[u.uid] = u.username || ''; });
450
451
 
451
- const events = reservations
452
+ const itemById = {};
453
+ items.forEach(it => { itemById[it.id] = it; });
454
+
455
+ const events = allReservations
452
456
  .filter(r => r.status !== 'rejected' && r.status !== 'cancelled')
453
- .map(r => toEvent(r, chosenItem, nameByUid[r.uid], isApprover || showRequesterToAll));
457
+ .map(r => {
458
+ const item = itemById[r.itemId];
459
+ // Include item name in title so "all items" view is readable
460
+ const requesterName = nameByUid[r.uid] || '';
461
+ return toEvent(r, item, requesterName, isApprover || showRequesterToAll);
462
+ });
454
463
 
455
464
  const canUserCreate = req.uid ? await canCreate(req.uid, settings) : false;
456
465
 
466
+ // We expose minimal reservation data for availability checks in the modal
467
+ const blocks = allReservations
468
+ .filter(r => statusBlocksItem(r.status))
469
+ .map(r => ({
470
+ itemId: r.itemId,
471
+ startMs: r.startMs,
472
+ endMs: r.endMs,
473
+ status: r.status,
474
+ }));
475
+
457
476
  res.render('equipment-calendar/calendar', {
458
477
  title: 'Réservation de matériel',
459
- items: itemsView,
460
- chosenItemId: chosenItem ? chosenItem.id : '',
461
- chosenItemName: chosenItem ? chosenItem.name : '',
462
- chosenItemPriceCents: chosenItem ? chosenItem.priceCents : 0,
463
- chosenItemLocation: chosenItem ? chosenItem.location : '',
478
+ items,
464
479
  view,
465
480
  tz,
466
481
  startISO: start.toISO(),
467
482
  endISO: end.toISO(),
468
483
  initialDateISO: start.toISODate(),
469
- eventsB64: Buffer.from(JSON.stringify(events), 'utf8').toString('base64'),
470
484
  canCreate: canUserCreate,
471
485
  canCreateJs: canUserCreate ? 'true' : 'false',
472
486
  isApprover,
487
+ // events are base64 encoded to avoid template escaping issues
488
+ eventsB64: Buffer.from(JSON.stringify(events), 'utf8').toString('base64'),
489
+ blocksB64: Buffer.from(JSON.stringify(blocks), 'utf8').toString('base64'),
490
+ itemsB64: Buffer.from(JSON.stringify(items.map(i => ({ id: i.id, name: i.name, location: i.location }))), 'utf8').toString('base64'),
473
491
  csrf: req.csrfToken,
474
- forumUrl: nconf.get('url'),
475
492
  });
476
493
  }
477
494
 
495
+
478
496
  // --- Approvals page ---
479
497
  async function renderApprovalsPage(req, res) {
480
498
  const settings = await getSettings();
@@ -667,4 +685,35 @@ async function handleRejectReservation(req, res) {
667
685
  }
668
686
  }
669
687
 
688
+
689
+ async function handleAdminSave(req, res) {
690
+ try {
691
+ // Simple admin check (avoid relying on client-side require/settings)
692
+ const isAdmin = req.uid ? await groups.isMember(req.uid, 'administrators') : false;
693
+ if (!isAdmin) {
694
+ return helpers.notAllowed(req, res);
695
+ }
696
+
697
+ const values = {
698
+ creatorGroups: String(req.body.creatorGroups || DEFAULT_SETTINGS.creatorGroups),
699
+ approverGroup: String(req.body.approverGroup || DEFAULT_SETTINGS.approverGroup),
700
+ notifyGroup: String(req.body.notifyGroup || DEFAULT_SETTINGS.notifyGroup),
701
+ itemsJson: String(req.body.itemsJson || DEFAULT_SETTINGS.itemsJson),
702
+ ha_clientId: String(req.body.ha_clientId || ''),
703
+ ha_clientSecret: String(req.body.ha_clientSecret || ''),
704
+ ha_organizationSlug: String(req.body.ha_organizationSlug || ''),
705
+ ha_returnUrl: String(req.body.ha_returnUrl || ''),
706
+ ha_webhookSecret: String(req.body.ha_webhookSecret || ''),
707
+ defaultView: String(req.body.defaultView || DEFAULT_SETTINGS.defaultView),
708
+ timezone: String(req.body.timezone || DEFAULT_SETTINGS.timezone),
709
+ showRequesterToAll: (req.body.showRequesterToAll === '1' || req.body.showRequesterToAll === 'on') ? '1' : '0',
710
+ };
711
+
712
+ await meta.settings.set(SETTINGS_KEY, values);
713
+ return res.redirect('/admin/plugins/equipment-calendar');
714
+ } catch (e) {
715
+ return res.status(500).send(e.message || 'error');
716
+ }
717
+ }
718
+
670
719
  module.exports = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Equipment reservation calendar for NodeBB (FullCalendar, approvals, HelloAsso payments)",
5
5
  "main": "library.js",
6
6
  "scripts": {
package/plugin.json CHANGED
@@ -26,7 +26,6 @@
26
26
  "scripts": [
27
27
  "public/js/client.js"
28
28
  ],
29
- "acpScripts": [
30
- "public/js/admin.js"
31
- ]
29
+ "version": "0.2.1",
30
+ "minver": "4.7.1"
32
31
  }
@@ -1,55 +1,20 @@
1
1
  'use strict';
2
- /* global window, document, socket, app */
3
2
 
4
- (function () {
5
- function byId(id) { return document.getElementById(id); }
3
+ /* global define */
6
4
 
7
- function collect() {
8
- return {
9
- creatorGroups: byId('creatorGroups') ? byId('creatorGroups').value : '',
10
- approverGroup: byId('approverGroup') ? byId('approverGroup').value : '',
11
- notifyGroup: byId('notifyGroup') ? byId('notifyGroup').value : '',
12
- itemsJson: byId('itemsJson') ? byId('itemsJson').value : '[]',
13
- ha_clientId: byId('ha_clientId') ? byId('ha_clientId').value : '',
14
- ha_clientSecret: byId('ha_clientSecret') ? byId('ha_clientSecret').value : '',
15
- ha_organizationSlug: byId('ha_organizationSlug') ? byId('ha_organizationSlug').value : '',
16
- ha_returnUrl: byId('ha_returnUrl') ? byId('ha_returnUrl').value : '',
17
- ha_webhookSecret: byId('ha_webhookSecret') ? byId('ha_webhookSecret').value : '',
18
- defaultView: byId('defaultView') ? byId('defaultView').value : 'dayGridMonth',
19
- timezone: byId('timezone') ? byId('timezone').value : 'Europe/Paris',
20
- showRequesterToAll: (byId('showRequesterToAll') && byId('showRequesterToAll').checked) ? '1' : '0',
21
- };
22
- }
5
+ define('admin/plugins/equipment-calendar', ['jquery', 'settings', 'alerts'], function ($, Settings, alerts) {
6
+ const Admin = {};
23
7
 
24
- function onReady(fn) {
25
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
26
- else fn();
27
- }
8
+ Admin.init = function () {
9
+ const $container = $('.equipment-calendar-settings');
10
+ Settings.load('equipmentCalendar', $container);
28
11
 
29
- onReady(function () {
30
- const btn = byId('save');
31
- if (!btn) return;
32
-
33
- btn.addEventListener('click', function () {
34
- if (typeof socket === 'undefined' || !socket.emit) {
35
- const msg = 'Socket NodeBB indisponible (socket.emit). Vérifie que tu es bien dans l\'ACP.';
36
- if (window.app && window.app.alertError) window.app.alertError(msg);
37
- else alert(msg);
38
- return;
39
- }
40
-
41
- const payload = collect();
42
-
43
- socket.emit('admin.settings.save', { hash: 'equipmentCalendar', values: payload }, function (err) {
44
- if (err) {
45
- const m = (err.message || err) + '';
46
- if (window.app && window.app.alertError) return window.app.alertError(m);
47
- alert(m);
48
- return;
49
- }
50
- if (window.app && window.app.alertSuccess) return window.app.alertSuccess('Sauvegardé');
51
- alert('Sauvegardé');
12
+ $('#save').on('click', function () {
13
+ Settings.save('equipmentCalendar', $container, function () {
14
+ alerts.success('Sauvegardé');
52
15
  });
53
16
  });
54
- });
55
- }());
17
+ };
18
+
19
+ return Admin;
20
+ });
@@ -1,18 +1,85 @@
1
1
  'use strict';
2
- /* global window, document, FullCalendar */
2
+ /* global window, document, FullCalendar, bootstrap */
3
3
 
4
4
  (function () {
5
- function submitCreate(startISO, endISO) {
5
+ function overlaps(aStartMs, aEndMs, bStartMs, bEndMs) {
6
+ return aStartMs < bEndMs && aEndMs > bStartMs;
7
+ }
8
+
9
+ function fmt(dt) {
10
+ try {
11
+ return new Intl.DateTimeFormat('fr-FR', { dateStyle: 'full', timeStyle: 'short' }).format(dt);
12
+ } catch (e) {
13
+ return dt.toISOString();
14
+ }
15
+ }
16
+
17
+ function openModal(startISO, endISO) {
6
18
  const form = document.getElementById('ec-create-form');
7
19
  if (!form) return;
20
+
21
+ const startDate = new Date(startISO);
22
+ const endDate = new Date(endISO);
23
+ const startMs = startDate.getTime();
24
+ const endMs = endDate.getTime();
25
+
26
+ // write hidden ISO values
8
27
  const startInput = form.querySelector('input[name="start"]');
9
28
  const endInput = form.querySelector('input[name="end"]');
10
- if (startInput) startInput.value = startISO;
11
- if (endInput) endInput.value = endISO;
12
- form.submit();
29
+ if (startInput) startInput.value = startDate.toISOString();
30
+ if (endInput) endInput.value = endDate.toISOString();
31
+
32
+ // displays
33
+ const sDisp = document.getElementById('ecStartDisplay');
34
+ const eDisp = document.getElementById('ecEndDisplay');
35
+ if (sDisp) sDisp.value = fmt(startDate);
36
+ if (eDisp) eDisp.value = fmt(endDate);
37
+
38
+ // compute availability
39
+ const blocks = Array.isArray(window.EC_BLOCKS) ? window.EC_BLOCKS : [];
40
+ const items = Array.isArray(window.EC_ITEMS) ? window.EC_ITEMS : [];
41
+
42
+ const blockedByItem = {};
43
+ for (const b of blocks) {
44
+ if (!blockedByItem[b.itemId]) blockedByItem[b.itemId] = [];
45
+ blockedByItem[b.itemId].push(b);
46
+ }
47
+
48
+ const available = [];
49
+ for (const it of items) {
50
+ const bks = blockedByItem[it.id] || [];
51
+ const hasOverlap = bks.some(b => overlaps(startMs, endMs, Number(b.startMs), Number(b.endMs)));
52
+ if (!hasOverlap) available.push(it);
53
+ }
54
+
55
+ const select = document.getElementById('ecItemSelect');
56
+ const hint = document.getElementById('ecItemHint');
57
+ const noAvail = document.getElementById('ecNoAvailability');
58
+ const submitBtn = document.getElementById('ecSubmitBtn');
59
+
60
+ if (select) {
61
+ select.innerHTML = '';
62
+ for (const it of available) {
63
+ const opt = document.createElement('option');
64
+ opt.value = it.id;
65
+ opt.textContent = it.location ? `${it.name} — ${it.location}` : it.name;
66
+ select.appendChild(opt);
67
+ }
68
+ }
69
+
70
+ const hasAny = available.length > 0;
71
+ if (hint) hint.textContent = hasAny ? '' : '';
72
+ if (noAvail) noAvail.classList.toggle('d-none', hasAny);
73
+ if (submitBtn) submitBtn.disabled = !hasAny;
74
+
75
+ // show modal
76
+ const modalEl = document.getElementById('ecReserveModal');
77
+ if (!modalEl) return;
78
+ const modal = bootstrap ? bootstrap.Modal.getOrCreateInstance(modalEl) : null;
79
+ if (modal) modal.show();
13
80
  }
14
81
 
15
- function init() {
82
+ function initCalendar() {
16
83
  const el = document.getElementById('equipment-calendar');
17
84
  if (!el || typeof FullCalendar === 'undefined') return;
18
85
 
@@ -28,12 +95,13 @@
28
95
  selectMirror: true,
29
96
  events: events,
30
97
  select: function (info) {
31
- submitCreate(info.startStr, info.endStr);
98
+ openModal(info.startStr, info.endStr);
32
99
  },
33
100
  dateClick: function (info) {
101
+ // default 1 hour
34
102
  const start = info.date;
35
103
  const end = new Date(start.getTime() + 60 * 60 * 1000);
36
- submitCreate(start.toISOString(), end.toISOString());
104
+ openModal(start.toISOString(), end.toISOString());
37
105
  },
38
106
  headerToolbar: {
39
107
  left: 'prev,next today',
@@ -46,8 +114,8 @@
46
114
  }
47
115
 
48
116
  if (document.readyState === 'loading') {
49
- document.addEventListener('DOMContentLoaded', init);
117
+ document.addEventListener('DOMContentLoaded', initCalendar);
50
118
  } else {
51
- init();
119
+ initCalendar();
52
120
  }
53
121
  }());
@@ -1,64 +1,65 @@
1
1
  <div class="acp-page-container">
2
2
  <h1>Equipment Calendar</h1>
3
3
 
4
- <div class="alert alert-warning">
5
- Les champs "Matériel" doivent être un JSON valide (array). Exemple :
6
- <pre class="mb-0">[
7
- { "id": "cam1", "name": "Caméra A", "priceCents": 5000, "location": "Stock A", "active": true },
8
- { "id": "light1", "name": "Projecteur", "priceCents": 2000, "location": "Stock B", "active": true }
4
+ <form method="post" action="/admin/plugins/equipment-calendar/save" class="mb-3">
5
+ <input type="hidden" name="_csrf" value="{csrf}">
6
+
7
+ <div class="alert alert-warning">
8
+ Le champ "Matériel" doit être un JSON valide (array). Exemple :
9
+ <pre class="mb-0">[
10
+ { "id": "cam1", "name": "Caméra A", "priceCents": 50, "location": "Stock A", "active": true },
11
+ { "id": "light1", "name": "Projecteur", "priceCents": 20, "location": "Stock B", "active": true }
9
12
  ]</pre>
10
- </div>
13
+ <div class="mt-2">Note : <strong>priceCents est en euros</strong> dans ce plugin (ex: 50 = 50€).</div>
14
+ </div>
11
15
 
12
- <div class="row">
13
- <div class="col-lg-8">
14
- <div class="card card-body mb-3">
15
- <h5>Permissions</h5>
16
- <div class="mb-3">
17
- <label class="form-label">Groupes autorisés à créer (CSV)</label>
18
- <input id="creatorGroups" class="form-control" value="{settings.creatorGroups}">
19
- </div>
20
- <div class="mb-3">
21
- <label class="form-label">Groupe validateur</label>
22
- <input id="approverGroup" class="form-control" value="{settings.approverGroup}">
23
- </div>
24
- <div class="mb-3">
25
- <label class="form-label">Groupe notifié (emails/notifs)</label>
26
- <input id="notifyGroup" class="form-control" value="{settings.notifyGroup}">
27
- </div>
28
- <div class="form-check">
29
- <input class="form-check-input" type="checkbox" id="showRequesterToAll" {{{ if view_showRequesterToAll }}}checked{{{ end }}}>
30
- <label class="form-check-label" for="showRequesterToAll">Afficher le demandeur à tout le monde</label>
31
- </div>
16
+ <div class="card card-body mb-3">
17
+ <h5>Permissions</h5>
18
+ <div class="mb-3">
19
+ <label class="form-label">Groupes autorisés à créer (CSV)</label>
20
+ <input name="creatorGroups" class="form-control" value="{settings.creatorGroups}">
32
21
  </div>
33
-
34
- <div class="card card-body mb-3">
35
- <h5>Matériel</h5>
36
- <textarea id="itemsJson" class="form-control" rows="10">{settings.itemsJson}</textarea>
22
+ <div class="mb-3">
23
+ <label class="form-label">Groupe validateur</label>
24
+ <input name="approverGroup" class="form-control" value="{settings.approverGroup}">
37
25
  </div>
38
-
39
- <div class="card card-body mb-3">
40
- <h5>HelloAsso</h5>
41
- <div class="mb-3"><label class="form-label">Client ID</label><input id="ha_clientId" class="form-control" value="{settings.ha_clientId}"></div>
42
- <div class="mb-3"><label class="form-label">Client Secret</label><input id="ha_clientSecret" class="form-control" value="{settings.ha_clientSecret}"></div>
43
- <div class="mb-3"><label class="form-label">Organization Slug</label><input id="ha_organizationSlug" class="form-control" value="{settings.ha_organizationSlug}"></div>
44
- <div class="mb-3"><label class="form-label">Return URL</label><input id="ha_returnUrl" class="form-control" value="{settings.ha_returnUrl}"></div>
45
- <div class="mb-3"><label class="form-label">Webhook Secret (HMAC SHA256)</label><input id="ha_webhookSecret" class="form-control" value="{settings.ha_webhookSecret}"></div>
26
+ <div class="mb-3">
27
+ <label class="form-label">Groupe notifié (emails/notifs)</label>
28
+ <input name="notifyGroup" class="form-control" value="{settings.notifyGroup}">
46
29
  </div>
47
-
48
- <div class="card card-body mb-3">
49
- <h5>Calendrier</h5>
50
- <div class="mb-3"><label class="form-label">Vue par défaut</label>
51
- <select id="defaultView" class="form-select">
52
- <option value="dayGridMonth" {{{ if view_dayGridMonth }}}selected{{{ end }}}>Mois</option>
53
- <option value="timeGridWeek" {{{ if view_timeGridWeek }}}selected{{{ end }}}>Semaine</option>
54
- <option value="timeGridDay" {{{ if view_timeGridDay }}}selected{{{ end }}}>Jour</option>
55
- </select>
56
- </div>
57
- <div class="mb-3"><label class="form-label">Timezone</label><input id="timezone" class="form-control" value="{settings.timezone}"></div>
30
+ <div class="form-check">
31
+ <input class="form-check-input" type="checkbox" name="showRequesterToAll" id="showRequesterToAll" {{{ if view_showRequesterToAll }}}checked{{{ end }}}>
32
+ <label class="form-check-label" for="showRequesterToAll">Afficher le demandeur à tout le monde</label>
58
33
  </div>
34
+ </div>
59
35
 
60
- <button id="save" class="btn btn-primary">Sauvegarder</button>
36
+ <div class="card card-body mb-3">
37
+ <h5>Matériel</h5>
38
+ <textarea name="itemsJson" class="form-control" rows="10">{settings.itemsJson}</textarea>
61
39
  </div>
62
- </div>
63
- </div>
64
40
 
41
+ <div class="card card-body mb-3">
42
+ <h5>HelloAsso</h5>
43
+ <div class="mb-3"><label class="form-label">Client ID</label><input name="ha_clientId" class="form-control" value="{settings.ha_clientId}"></div>
44
+ <div class="mb-3"><label class="form-label">Client Secret</label><input name="ha_clientSecret" class="form-control" value="{settings.ha_clientSecret}"></div>
45
+ <div class="mb-3"><label class="form-label">Organization Slug</label><input name="ha_organizationSlug" class="form-control" value="{settings.ha_organizationSlug}"></div>
46
+ <div class="mb-3"><label class="form-label">Return URL</label><input name="ha_returnUrl" class="form-control" value="{settings.ha_returnUrl}"></div>
47
+ <div class="mb-3"><label class="form-label">Webhook Secret (HMAC SHA256)</label><input name="ha_webhookSecret" class="form-control" value="{settings.ha_webhookSecret}"></div>
48
+ </div>
49
+
50
+ <div class="card card-body mb-3">
51
+ <h5>Calendrier</h5>
52
+ <div class="mb-3">
53
+ <label class="form-label">Vue par défaut</label>
54
+ <select name="defaultView" class="form-select">
55
+ <option value="dayGridMonth" {{{ if view_dayGridMonth }}}selected{{{ end }}}>Mois</option>
56
+ <option value="timeGridWeek" {{{ if view_timeGridWeek }}}selected{{{ end }}}>Semaine</option>
57
+ <option value="timeGridDay" {{{ if view_timeGridDay }}}selected{{{ end }}}>Jour</option>
58
+ </select>
59
+ </div>
60
+ <div class="mb-3"><label class="form-label">Timezone</label><input name="timezone" class="form-control" value="{settings.timezone}"></div>
61
+ </div>
62
+
63
+ <button class="btn btn-primary" type="submit">Sauvegarder</button>
64
+ </form>
65
+ </div>
@@ -1,40 +1,10 @@
1
1
  <div class="equipment-calendar-page">
2
2
  <h1>Réservation de matériel</h1>
3
3
 
4
- <div class="mb-3">
5
- <form method="get" action="/equipment/calendar" class="d-flex gap-2 align-items-end">
6
- <div>
7
- <label class="form-label">Matériel</label>
8
- <select name="itemId" class="form-select" onchange="this.form.submit()">
9
- {{{ each items }}}
10
- <option value="{items.id}" {{{ if items.selected }}}selected{{{ end }}}>{items.name} — {items.location}</option>
11
- {{{ end }}}
12
- </select>
13
- </div>
14
- <div class="text-muted small">
15
- <div><strong>Lieu:</strong> {chosenItemLocation}</div>
16
- <div><strong>Prix:</strong> {chosenItemPriceCents} cts</div>
17
- </div>
18
- </form>
19
- </div>
20
-
21
4
  {{{ if canCreate }}}
22
- <form id="ec-create-form" method="post" action="/equipment/reservations/create" class="card card-body mb-3">
23
- <input type="hidden" name="_csrf" value="{csrf}">
24
- <input type="hidden" name="itemId" value="{chosenItemId}">
25
- <input type="hidden" name="start" value="">
26
- <input type="hidden" name="end" value="">
27
- <div class="row g-2 align-items-end">
28
- <div class="col-md-8">
29
- <label class="form-label">Note (optionnel)</label>
30
- <input class="form-control" type="text" name="notesUser" maxlength="2000" placeholder="Ex: besoin de trépied, etc.">
31
- </div>
32
- <div class="col-md-4">
33
- <button class="btn btn-primary w-100" type="submit" onclick="return false;">Sélectionne une date sur le calendrier</button>
34
- </div>
35
- </div>
36
- <div class="form-text">Clique sur une date ou sélectionne une plage sur le calendrier pour soumettre une demande.</div>
37
- </form>
5
+ <div class="alert alert-info">
6
+ Clique sur une date ou sélectionne une plage sur le calendrier pour faire une demande.
7
+ </div>
38
8
  {{{ else }}}
39
9
  <div class="alert alert-info">Tu peux consulter le calendrier, mais tu n’as pas les droits pour créer une demande.</div>
40
10
  {{{ end }}}
@@ -42,12 +12,53 @@
42
12
  <div class="card card-body">
43
13
  <div id="equipment-calendar"></div>
44
14
  </div>
15
+ </div>
16
+
17
+ <!-- Modal de réservation -->
18
+ <div class="modal fade" id="ecReserveModal" tabindex="-1" aria-hidden="true">
19
+ <div class="modal-dialog">
20
+ <div class="modal-content">
21
+ <form id="ec-create-form" method="post" action="/equipment/reservations/create">
22
+ <div class="modal-header">
23
+ <h5 class="modal-title">Demande de réservation</h5>
24
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
25
+ </div>
26
+ <div class="modal-body">
27
+ <input type="hidden" name="_csrf" value="{csrf}">
28
+ <input type="hidden" name="start" value="">
29
+ <input type="hidden" name="end" value="">
45
30
 
46
- {{{ if isApprover }}}
47
- <div class="mt-3">
48
- <a class="btn btn-outline-secondary" href="/equipment/approvals">Aller à la validation</a>
31
+ <div class="mb-3">
32
+ <label class="form-label">Début</label>
33
+ <input type="text" class="form-control" id="ecStartDisplay" readonly>
34
+ </div>
35
+ <div class="mb-3">
36
+ <label class="form-label">Fin</label>
37
+ <input type="text" class="form-control" id="ecEndDisplay" readonly>
38
+ </div>
39
+
40
+ <div class="mb-3">
41
+ <label class="form-label">Matériel disponible</label>
42
+ <select class="form-select" name="itemId" id="ecItemSelect" required></select>
43
+ <div class="form-text" id="ecItemHint"></div>
44
+ </div>
45
+
46
+ <div class="mb-3">
47
+ <label class="form-label">Note (optionnel)</label>
48
+ <input class="form-control" type="text" name="notesUser" maxlength="2000" placeholder="Ex: besoin de trépied, etc.">
49
+ </div>
50
+
51
+ <div class="alert alert-warning d-none" id="ecNoAvailability">
52
+ Aucun matériel n’est disponible sur cette plage.
53
+ </div>
54
+ </div>
55
+ <div class="modal-footer">
56
+ <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuler</button>
57
+ <button type="submit" class="btn btn-primary" id="ecSubmitBtn">Envoyer la demande</button>
58
+ </div>
59
+ </form>
60
+ </div>
49
61
  </div>
50
- {{{ end }}}
51
62
  </div>
52
63
 
53
64
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/6.1.19/index.global.min.js"></script>
@@ -55,6 +66,8 @@
55
66
 
56
67
  <script>
57
68
  window.EC_EVENTS = JSON.parse(atob('{eventsB64}'));
69
+ window.EC_BLOCKS = JSON.parse(atob('{blocksB64}'));
70
+ window.EC_ITEMS = JSON.parse(atob('{itemsB64}'));
58
71
  window.EC_INITIAL_DATE = "{initialDateISO}";
59
72
  window.EC_INITIAL_VIEW = "{view}";
60
73
  window.EC_TZ = "{tz}";