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 +67 -18
- package/package.json +1 -1
- package/plugin.json +2 -3
- package/public/js/admin.js +13 -48
- package/public/js/client.js +78 -10
- package/public/templates/admin/plugins/equipment-calendar.tpl +53 -52
- package/public/templates/equipment-calendar/calendar.tpl +50 -37
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
|
|
442
|
-
const
|
|
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(
|
|
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
|
|
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 =>
|
|
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
|
|
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
package/plugin.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,64 +1,65 @@
|
|
|
1
1
|
<div class="acp-page-container">
|
|
2
2
|
<h1>Equipment Calendar</h1>
|
|
3
3
|
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
+
<div class="mt-2">Note : <strong>priceCents est en euros</strong> dans ce plugin (ex: 50 = 50€).</div>
|
|
14
|
+
</div>
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<div class="
|
|
15
|
-
<
|
|
16
|
-
<
|
|
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
|
-
|
|
35
|
-
<
|
|
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
|
-
|
|
40
|
-
<
|
|
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
|
-
|
|
49
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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}";
|