nodebb-plugin-equipment-calendar 0.3.0 → 0.4.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/library.js
CHANGED
|
@@ -402,6 +402,7 @@ async function renderAdminPage(req, res) {
|
|
|
402
402
|
res.render('admin/plugins/equipment-calendar', {
|
|
403
403
|
title: 'Equipment Calendar',
|
|
404
404
|
settings,
|
|
405
|
+
saved: req.query && String(req.query.saved || '') === '1',
|
|
405
406
|
view_dayGridMonth: (settings.defaultView || 'dayGridMonth') === 'dayGridMonth',
|
|
406
407
|
view_timeGridWeek: (settings.defaultView || '') === 'timeGridWeek',
|
|
407
408
|
view_timeGridDay: (settings.defaultView || '') === 'timeGridDay',
|
|
@@ -708,7 +709,7 @@ async function handleAdminSave(req, res) {
|
|
|
708
709
|
};
|
|
709
710
|
|
|
710
711
|
await meta.settings.set(SETTINGS_KEY, values);
|
|
711
|
-
return res.redirect('/admin/plugins/equipment-calendar');
|
|
712
|
+
return res.redirect('/admin/plugins/equipment-calendar?saved=1');
|
|
712
713
|
} catch (e) {
|
|
713
714
|
return res.status(500).send(e.message || 'error');
|
|
714
715
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
/* global window, document, FullCalendar,
|
|
2
|
+
/* global window, document, FullCalendar, bootbox */
|
|
3
3
|
|
|
4
4
|
(function () {
|
|
5
5
|
function overlaps(aStartMs, aEndMs, bStartMs, bEndMs) {
|
|
@@ -14,28 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function
|
|
18
|
-
const form = document.getElementById('ec-create-form');
|
|
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
|
|
27
|
-
const startInput = form.querySelector('input[name="start"]');
|
|
28
|
-
const endInput = form.querySelector('input[name="end"]');
|
|
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
|
|
17
|
+
function buildAvailability(startMs, endMs) {
|
|
39
18
|
const blocks = Array.isArray(window.EC_BLOCKS) ? window.EC_BLOCKS : [];
|
|
40
19
|
const items = Array.isArray(window.EC_ITEMS) ? window.EC_ITEMS : [];
|
|
41
20
|
|
|
@@ -51,32 +30,99 @@
|
|
|
51
30
|
const hasOverlap = bks.some(b => overlaps(startMs, endMs, Number(b.startMs), Number(b.endMs)));
|
|
52
31
|
if (!hasOverlap) available.push(it);
|
|
53
32
|
}
|
|
33
|
+
return available;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function submitReservation(startISO, endISO, itemId, notes) {
|
|
37
|
+
const form = document.getElementById('ec-create-form');
|
|
38
|
+
if (!form) return;
|
|
39
|
+
|
|
40
|
+
const startInput = form.querySelector('input[name="start"]');
|
|
41
|
+
const endInput = form.querySelector('input[name="end"]');
|
|
42
|
+
const itemInput = form.querySelector('input[name="itemId"]');
|
|
43
|
+
const notesInput = form.querySelector('input[name="notesUser"]');
|
|
44
|
+
|
|
45
|
+
if (startInput) startInput.value = startISO;
|
|
46
|
+
if (endInput) endInput.value = endISO;
|
|
47
|
+
if (itemInput) itemInput.value = itemId;
|
|
48
|
+
if (notesInput) notesInput.value = notes || '';
|
|
49
|
+
|
|
50
|
+
form.submit();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function openNodeBBModal(startISO, endISO) {
|
|
54
|
+
const startDate = new Date(startISO);
|
|
55
|
+
const endDate = new Date(endISO);
|
|
56
|
+
const startMs = startDate.getTime();
|
|
57
|
+
const endMs = endDate.getTime();
|
|
54
58
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
59
|
+
const available = buildAvailability(startMs, endMs);
|
|
60
|
+
|
|
61
|
+
if (!available.length) {
|
|
62
|
+
if (typeof bootbox !== 'undefined') {
|
|
63
|
+
bootbox.alert('Aucun matériel n’est disponible sur cette plage.');
|
|
64
|
+
} else {
|
|
65
|
+
alert('Aucun matériel n’est disponible sur cette plage.');
|
|
67
66
|
}
|
|
67
|
+
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
// Build modal HTML
|
|
71
|
+
let optionsHtml = '';
|
|
72
|
+
for (const it of available) {
|
|
73
|
+
const label = it.location ? `${it.name} — ${it.location}` : it.name;
|
|
74
|
+
optionsHtml += `<option value="${it.id}">${label}</option>`;
|
|
75
|
+
}
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
const body = `
|
|
78
|
+
<div class="mb-3">
|
|
79
|
+
<label class="form-label">Début</label>
|
|
80
|
+
<input class="form-control" type="text" value="${fmt(startDate)}" readonly>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="mb-3">
|
|
83
|
+
<label class="form-label">Fin</label>
|
|
84
|
+
<input class="form-control" type="text" value="${fmt(endDate)}" readonly>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="mb-3">
|
|
87
|
+
<label class="form-label">Matériel</label>
|
|
88
|
+
<select class="form-select" id="ec-modal-item">
|
|
89
|
+
${optionsHtml}
|
|
90
|
+
</select>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="mb-3">
|
|
93
|
+
<label class="form-label">Note (optionnel)</label>
|
|
94
|
+
<input class="form-control" type="text" id="ec-modal-notes" maxlength="2000" placeholder="Ex: besoin de trépied, etc.">
|
|
95
|
+
</div>
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
if (typeof bootbox === 'undefined') {
|
|
99
|
+
// Fallback without bootbox (should be rare on NodeBB pages)
|
|
100
|
+
const itemId = available[0].id;
|
|
101
|
+
submitReservation(startDate.toISOString(), endDate.toISOString(), itemId, '');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
bootbox.dialog({
|
|
106
|
+
title: 'Demande de réservation',
|
|
107
|
+
message: body,
|
|
108
|
+
buttons: {
|
|
109
|
+
cancel: {
|
|
110
|
+
label: 'Annuler',
|
|
111
|
+
className: 'btn-outline-secondary',
|
|
112
|
+
},
|
|
113
|
+
ok: {
|
|
114
|
+
label: 'Envoyer la demande',
|
|
115
|
+
className: 'btn-primary',
|
|
116
|
+
callback: function () {
|
|
117
|
+
const itemEl = document.getElementById('ec-modal-item');
|
|
118
|
+
const notesEl = document.getElementById('ec-modal-notes');
|
|
119
|
+
const itemId = itemEl ? itemEl.value : available[0].id;
|
|
120
|
+
const notes = notesEl ? notesEl.value : '';
|
|
121
|
+
submitReservation(startDate.toISOString(), endDate.toISOString(), itemId, notes);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
80
126
|
}
|
|
81
127
|
|
|
82
128
|
function initCalendar() {
|
|
@@ -94,15 +140,20 @@
|
|
|
94
140
|
selectable: window.EC_CAN_CREATE === true,
|
|
95
141
|
selectMirror: true,
|
|
96
142
|
events: events,
|
|
143
|
+
|
|
144
|
+
// Range selection (drag)
|
|
97
145
|
select: function (info) {
|
|
98
|
-
|
|
146
|
+
openNodeBBModal(info.startStr, info.endStr);
|
|
99
147
|
},
|
|
148
|
+
|
|
149
|
+
// Single date click
|
|
100
150
|
dateClick: function (info) {
|
|
101
|
-
//
|
|
151
|
+
// Default: 1h slot
|
|
102
152
|
const start = info.date;
|
|
103
153
|
const end = new Date(start.getTime() + 60 * 60 * 1000);
|
|
104
|
-
|
|
154
|
+
openNodeBBModal(start.toISOString(), end.toISOString());
|
|
105
155
|
},
|
|
156
|
+
|
|
106
157
|
headerToolbar: {
|
|
107
158
|
left: 'prev,next today',
|
|
108
159
|
center: 'title',
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
<div class="acp-page-container">
|
|
2
2
|
<h1>Equipment Calendar</h1>
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
{{{ if saved }}}
|
|
5
|
+
<div class="alert alert-success">Paramètres enregistrés.</div>
|
|
6
|
+
<script>
|
|
7
|
+
(function () {
|
|
8
|
+
try {
|
|
9
|
+
if (window.app && window.app.alertSuccess) {
|
|
10
|
+
window.app.alertSuccess('Paramètres enregistrés');
|
|
11
|
+
}
|
|
12
|
+
} catch (e) {}
|
|
13
|
+
}());
|
|
14
|
+
</script>
|
|
15
|
+
{{{ end }}}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
<form id="ec-admin-form" method="post" action="/admin/plugins/equipment-calendar/save" class="mb-3">
|
|
5
19
|
<input type="hidden" name="_csrf" value="{config.csrf_token}">
|
|
6
20
|
|
|
7
21
|
<div class="alert alert-warning">
|
|
@@ -60,6 +74,25 @@
|
|
|
60
74
|
<div class="mb-3"><label class="form-label">Timezone</label><input name="timezone" class="form-control" value="{settings.timezone}"></div>
|
|
61
75
|
</div>
|
|
62
76
|
|
|
63
|
-
<button class="btn btn-primary" type="
|
|
77
|
+
<button id="ec-save" class="btn btn-primary" type="button">Sauvegarder</button>
|
|
64
78
|
</form>
|
|
65
79
|
</div>
|
|
80
|
+
|
|
81
|
+
<script>
|
|
82
|
+
(function () {
|
|
83
|
+
function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
|
|
84
|
+
ready(function () {
|
|
85
|
+
var btn = document.getElementById('ec-save');
|
|
86
|
+
var form = document.getElementById('ec-admin-form');
|
|
87
|
+
if (!btn || !form) { return; }
|
|
88
|
+
btn.addEventListener('click', function () {
|
|
89
|
+
try {
|
|
90
|
+
form.submit();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(e);
|
|
93
|
+
alert('Erreur lors de la soumission du formulaire (voir console).');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}());
|
|
98
|
+
</script>
|
|
@@ -12,56 +12,18 @@
|
|
|
12
12
|
<div class="card card-body">
|
|
13
13
|
<div id="equipment-calendar"></div>
|
|
14
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="{config.csrf_token}">
|
|
28
|
-
<input type="hidden" name="start" value="">
|
|
29
|
-
<input type="hidden" name="end" value="">
|
|
30
|
-
|
|
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
15
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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>
|
|
61
|
-
</div>
|
|
16
|
+
<!-- Formulaire de soumission (utilisé par la modale JS) -->
|
|
17
|
+
<form id="ec-create-form" method="post" action="/equipment/reservations/create" class="d-none">
|
|
18
|
+
<input type="hidden" name="_csrf" value="{config.csrf_token}">
|
|
19
|
+
<input type="hidden" name="start" value="">
|
|
20
|
+
<input type="hidden" name="end" value="">
|
|
21
|
+
<input type="hidden" name="itemId" value="">
|
|
22
|
+
<input type="hidden" name="notesUser" value="">
|
|
23
|
+
</form>
|
|
62
24
|
</div>
|
|
63
25
|
|
|
64
|
-
<script src="https://
|
|
26
|
+
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js"></script>
|
|
65
27
|
<script src="/plugins/nodebb-plugin-equipment-calendar/js/client.js"></script>
|
|
66
28
|
|
|
67
29
|
<script>
|