nodebb-plugin-equipment-calendar 0.2.7 → 0.2.8
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
|
@@ -413,14 +413,8 @@ async function renderCalendarPage(req, res) {
|
|
|
413
413
|
const settings = await getSettings();
|
|
414
414
|
const items = parseItems(settings.itemsJson).filter(i => i.active);
|
|
415
415
|
|
|
416
|
-
const rawItems = items;
|
|
417
|
-
|
|
418
416
|
const tz = settings.timezone || 'Europe/Paris';
|
|
419
417
|
|
|
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
418
|
// Determine range to render
|
|
425
419
|
const now = DateTime.now().setZone(tz);
|
|
426
420
|
const view = String(req.query.view || settings.defaultView || 'dayGridMonth');
|
|
@@ -438,43 +432,66 @@ async function renderCalendarPage(req, res) {
|
|
|
438
432
|
end = now.endOf('month').plus({ days: 1 });
|
|
439
433
|
}
|
|
440
434
|
|
|
441
|
-
// Load reservations for
|
|
442
|
-
const
|
|
435
|
+
// Load reservations for ALL items within range (so we can build availability client-side without extra requests)
|
|
436
|
+
const allReservations = [];
|
|
437
|
+
for (const it of items) {
|
|
438
|
+
// eslint-disable-next-line no-await-in-loop
|
|
439
|
+
const resForItem = await listReservationsForRange(it.id, start.toMillis(), end.toMillis());
|
|
440
|
+
for (const r of resForItem) allReservations.push(r);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
443
|
const showRequesterToAll = String(settings.showRequesterToAll) === '1';
|
|
444
444
|
const isApprover = req.uid ? await canApprove(req.uid, settings) : false;
|
|
445
445
|
|
|
446
|
-
const requesterUids = Array.from(new Set(
|
|
446
|
+
const requesterUids = Array.from(new Set(allReservations.map(r => r.uid))).filter(Boolean);
|
|
447
447
|
const users = requesterUids.length ? await user.getUsersData(requesterUids) : [];
|
|
448
448
|
const nameByUid = {};
|
|
449
449
|
(users || []).forEach(u => { nameByUid[u.uid] = u.username || ''; });
|
|
450
450
|
|
|
451
|
-
const
|
|
451
|
+
const itemById = {};
|
|
452
|
+
items.forEach(it => { itemById[it.id] = it; });
|
|
453
|
+
|
|
454
|
+
const events = allReservations
|
|
452
455
|
.filter(r => r.status !== 'rejected' && r.status !== 'cancelled')
|
|
453
|
-
.map(r =>
|
|
456
|
+
.map(r => {
|
|
457
|
+
const item = itemById[r.itemId];
|
|
458
|
+
// Include item name in title so "all items" view is readable
|
|
459
|
+
const requesterName = nameByUid[r.uid] || '';
|
|
460
|
+
return toEvent(r, item, requesterName, isApprover || showRequesterToAll);
|
|
461
|
+
});
|
|
454
462
|
|
|
455
463
|
const canUserCreate = req.uid ? await canCreate(req.uid, settings) : false;
|
|
456
464
|
|
|
465
|
+
// We expose minimal reservation data for availability checks in the modal
|
|
466
|
+
const blocks = allReservations
|
|
467
|
+
.filter(r => statusBlocksItem(r.status))
|
|
468
|
+
.map(r => ({
|
|
469
|
+
itemId: r.itemId,
|
|
470
|
+
startMs: r.startMs,
|
|
471
|
+
endMs: r.endMs,
|
|
472
|
+
status: r.status,
|
|
473
|
+
}));
|
|
474
|
+
|
|
457
475
|
res.render('equipment-calendar/calendar', {
|
|
458
476
|
title: 'Réservation de matériel',
|
|
459
|
-
items
|
|
460
|
-
chosenItemId: chosenItem ? chosenItem.id : '',
|
|
461
|
-
chosenItemName: chosenItem ? chosenItem.name : '',
|
|
462
|
-
chosenItemPriceCents: chosenItem ? chosenItem.priceCents : 0,
|
|
463
|
-
chosenItemLocation: chosenItem ? chosenItem.location : '',
|
|
477
|
+
items,
|
|
464
478
|
view,
|
|
465
479
|
tz,
|
|
466
480
|
startISO: start.toISO(),
|
|
467
481
|
endISO: end.toISO(),
|
|
468
482
|
initialDateISO: start.toISODate(),
|
|
469
|
-
eventsB64: Buffer.from(JSON.stringify(events), 'utf8').toString('base64'),
|
|
470
483
|
canCreate: canUserCreate,
|
|
471
484
|
canCreateJs: canUserCreate ? 'true' : 'false',
|
|
472
485
|
isApprover,
|
|
486
|
+
// events are base64 encoded to avoid template escaping issues
|
|
487
|
+
eventsB64: Buffer.from(JSON.stringify(events), 'utf8').toString('base64'),
|
|
488
|
+
blocksB64: Buffer.from(JSON.stringify(blocks), 'utf8').toString('base64'),
|
|
489
|
+
itemsB64: Buffer.from(JSON.stringify(items.map(i => ({ id: i.id, name: i.name, location: i.location }))), 'utf8').toString('base64'),
|
|
473
490
|
csrf: req.csrfToken,
|
|
474
|
-
forumUrl: nconf.get('url'),
|
|
475
491
|
});
|
|
476
492
|
}
|
|
477
493
|
|
|
494
|
+
|
|
478
495
|
// --- Approvals page ---
|
|
479
496
|
async function renderApprovalsPage(req, res) {
|
|
480
497
|
const settings = await getSettings();
|
package/package.json
CHANGED
package/public/js/admin.js
CHANGED
|
@@ -1,55 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
/* global window, document, socket, app */
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
function byId(id) { return document.getElementById(id); }
|
|
3
|
+
/* global define */
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
8
|
+
Admin.init = function () {
|
|
9
|
+
const $container = $('.equipment-calendar-settings');
|
|
10
|
+
Settings.load('equipmentCalendar', $container);
|
|
28
11
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
});
|
package/public/js/client.js
CHANGED
|
@@ -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
|
|
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 =
|
|
11
|
-
if (endInput) endInput.value =
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
117
|
+
document.addEventListener('DOMContentLoaded', initCalendar);
|
|
50
118
|
} else {
|
|
51
|
-
|
|
119
|
+
initCalendar();
|
|
52
120
|
}
|
|
53
121
|
}());
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<div class="acp-page-container">
|
|
1
|
+
<div class="acp-page-container equipment-calendar-settings">
|
|
2
2
|
<h1>Equipment Calendar</h1>
|
|
3
3
|
|
|
4
4
|
<div class="alert alert-warning">
|
|
5
|
-
|
|
5
|
+
Le champ "Matériel" doit être un JSON valide (array). Exemple :
|
|
6
6
|
<pre class="mb-0">[
|
|
7
7
|
{ "id": "cam1", "name": "Caméra A", "priceCents": 5000, "location": "Stock A", "active": true },
|
|
8
8
|
{ "id": "light1", "name": "Projecteur", "priceCents": 2000, "location": "Stock B", "active": true }
|
|
@@ -13,48 +13,53 @@
|
|
|
13
13
|
<div class="col-lg-8">
|
|
14
14
|
<div class="card card-body mb-3">
|
|
15
15
|
<h5>Permissions</h5>
|
|
16
|
+
|
|
16
17
|
<div class="mb-3">
|
|
17
18
|
<label class="form-label">Groupes autorisés à créer (CSV)</label>
|
|
18
|
-
<input
|
|
19
|
+
<input class="form-control" data-key="creatorGroups" placeholder="registered-users,staff">
|
|
19
20
|
</div>
|
|
21
|
+
|
|
20
22
|
<div class="mb-3">
|
|
21
23
|
<label class="form-label">Groupe validateur</label>
|
|
22
|
-
<input
|
|
24
|
+
<input class="form-control" data-key="approverGroup" placeholder="administrators">
|
|
23
25
|
</div>
|
|
26
|
+
|
|
24
27
|
<div class="mb-3">
|
|
25
28
|
<label class="form-label">Groupe notifié (emails/notifs)</label>
|
|
26
|
-
<input
|
|
29
|
+
<input class="form-control" data-key="notifyGroup" placeholder="administrators">
|
|
27
30
|
</div>
|
|
31
|
+
|
|
28
32
|
<div class="form-check">
|
|
29
|
-
<input class="form-check-input" type="checkbox" id="showRequesterToAll"
|
|
33
|
+
<input class="form-check-input" type="checkbox" data-key="showRequesterToAll" id="showRequesterToAll">
|
|
30
34
|
<label class="form-check-label" for="showRequesterToAll">Afficher le demandeur à tout le monde</label>
|
|
31
35
|
</div>
|
|
32
36
|
</div>
|
|
33
37
|
|
|
34
38
|
<div class="card card-body mb-3">
|
|
35
39
|
<h5>Matériel</h5>
|
|
36
|
-
<textarea
|
|
40
|
+
<textarea class="form-control" rows="10" data-key="itemsJson"></textarea>
|
|
37
41
|
</div>
|
|
38
42
|
|
|
39
43
|
<div class="card card-body mb-3">
|
|
40
44
|
<h5>HelloAsso</h5>
|
|
41
|
-
<div class="mb-3"><label class="form-label">Client ID</label><input
|
|
42
|
-
<div class="mb-3"><label class="form-label">Client Secret</label><input
|
|
43
|
-
<div class="mb-3"><label class="form-label">Organization Slug</label><input
|
|
44
|
-
<div class="mb-3"><label class="form-label">Return URL</label><input
|
|
45
|
-
<div class="mb-3"><label class="form-label">Webhook Secret (HMAC SHA256)</label><input
|
|
45
|
+
<div class="mb-3"><label class="form-label">Client ID</label><input class="form-control" data-key="ha_clientId"></div>
|
|
46
|
+
<div class="mb-3"><label class="form-label">Client Secret</label><input class="form-control" data-key="ha_clientSecret"></div>
|
|
47
|
+
<div class="mb-3"><label class="form-label">Organization Slug</label><input class="form-control" data-key="ha_organizationSlug"></div>
|
|
48
|
+
<div class="mb-3"><label class="form-label">Return URL</label><input class="form-control" data-key="ha_returnUrl"></div>
|
|
49
|
+
<div class="mb-3"><label class="form-label">Webhook Secret (HMAC SHA256)</label><input class="form-control" data-key="ha_webhookSecret"></div>
|
|
46
50
|
</div>
|
|
47
51
|
|
|
48
52
|
<div class="card card-body mb-3">
|
|
49
53
|
<h5>Calendrier</h5>
|
|
50
|
-
<div class="mb-3"
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
<option value="
|
|
54
|
-
<option value="
|
|
54
|
+
<div class="mb-3">
|
|
55
|
+
<label class="form-label">Vue par défaut</label>
|
|
56
|
+
<select class="form-select" data-key="defaultView">
|
|
57
|
+
<option value="dayGridMonth">Mois</option>
|
|
58
|
+
<option value="timeGridWeek">Semaine</option>
|
|
59
|
+
<option value="timeGridDay">Jour</option>
|
|
55
60
|
</select>
|
|
56
61
|
</div>
|
|
57
|
-
<div class="mb-3"><label class="form-label">Timezone</label><input
|
|
62
|
+
<div class="mb-3"><label class="form-label">Timezone</label><input class="form-control" data-key="timezone"></div>
|
|
58
63
|
</div>
|
|
59
64
|
|
|
60
65
|
<button id="save" class="btn btn-primary">Sauvegarder</button>
|
|
@@ -62,3 +67,6 @@
|
|
|
62
67
|
</div>
|
|
63
68
|
</div>
|
|
64
69
|
|
|
70
|
+
<script>
|
|
71
|
+
require(['admin/plugins/equipment-calendar'], function (m) { m.init(); });
|
|
72
|
+
</script>
|
|
@@ -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
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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}";
|