nodebb-plugin-equipment-calendar 1.0.0 → 1.0.3
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
|
@@ -29,7 +29,7 @@ const DEFAULT_SETTINGS = {
|
|
|
29
29
|
notifyGroup: 'administrators',
|
|
30
30
|
// JSON array of items: [{ "id": "cam1", "name": "Caméra A", "price": 5000, "location": "Stock A", "active": true }]
|
|
31
31
|
itemsJson: '[]',
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
ha_itemsFormType: '',
|
|
34
34
|
ha_itemsFormSlug: '',
|
|
35
35
|
|
|
@@ -275,47 +275,20 @@ async function fetchHelloAssoItems(settings) {
|
|
|
275
275
|
return out;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
function parseItems(itemsJson) {
|
|
279
|
-
try {
|
|
280
|
-
const arr = JSON.parse(itemsJson || '[]');
|
|
281
|
-
if (!Array.isArray(arr)) return [];
|
|
282
|
-
return arr.map(it => ({
|
|
283
|
-
id: String(it.id || '').trim(),
|
|
284
|
-
name: String(it.name || '').trim(),
|
|
285
|
-
price: Number(it.price || 0),
|
|
286
|
-
location: String(it.location || '').trim(),
|
|
287
|
-
active: it.active !== false,
|
|
288
|
-
})).filter(it => it.id && it.name);
|
|
289
|
-
} catch (e) {
|
|
290
|
-
return [];
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
278
|
|
|
294
|
-
async function getActiveItems(settings) {
|
|
295
|
-
const source = String(settings.itemsSource || 'manual');
|
|
296
|
-
if (source === 'helloasso') {
|
|
297
|
-
const rawItems = await fetchHelloAssoItems(settings); return (rawItems || []).map((it) => {
|
|
298
|
-
const id = String(it.id || it.itemId || it.reference || it.slug || it.name || '').trim();
|
|
299
|
-
const name = String(it.name || it.label || it.title || id).trim();
|
|
300
|
-
// Price handling is not displayed in the public calendar, keep a field if you want later
|
|
301
|
-
const priceRaw =
|
|
302
|
-
(it.price && (it.price.value || it.price.amount)) ||
|
|
303
|
-
(it.amount && (it.amount.value || it.amount)) ||
|
|
304
|
-
it.price;
|
|
305
|
-
const price = (typeof priceRaw === 'number' ? priceRaw : parseInt(priceRaw, 10)) || 0;
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
id: id || name,
|
|
309
|
-
name,
|
|
310
|
-
price,
|
|
311
|
-
active: true,
|
|
312
|
-
source: 'helloasso',
|
|
313
|
-
};
|
|
314
|
-
}).filter(i => i.id);
|
|
315
|
-
}
|
|
316
279
|
|
|
317
|
-
|
|
318
|
-
|
|
280
|
+
async function getActiveItems() {
|
|
281
|
+
const settings = await getSettings();
|
|
282
|
+
const rawItems = await fetchHelloAssoItems(settings);
|
|
283
|
+
// Normalize
|
|
284
|
+
const items = (Array.isArray(rawItems) ? rawItems : []).map((it) => ({
|
|
285
|
+
id: String(it.id || '').trim(),
|
|
286
|
+
name: String(it.name || '').trim(),
|
|
287
|
+
price: String(it.price || '0').trim(),
|
|
288
|
+
active: true,
|
|
289
|
+
})).filter(it => it.id && it.name);
|
|
290
|
+
|
|
291
|
+
return items;
|
|
319
292
|
}
|
|
320
293
|
|
|
321
294
|
async function getSettings() {
|
|
@@ -1426,8 +1399,7 @@ async function handleAdminSave(req, res) {
|
|
|
1426
1399
|
creatorGroups: String(req.body.creatorGroups || DEFAULT_SETTINGS.creatorGroups),
|
|
1427
1400
|
approverGroup: String(req.body.approverGroup || DEFAULT_SETTINGS.approverGroup),
|
|
1428
1401
|
notifyGroup: String(req.body.notifyGroup || DEFAULT_SETTINGS.notifyGroup),
|
|
1429
|
-
itemsJson:
|
|
1430
|
-
itemsSource: String(req.body.itemsSource || DEFAULT_SETTINGS.itemsSource),
|
|
1402
|
+
itemsJson: '[]',
|
|
1431
1403
|
ha_itemsFormType: String(req.body.ha_itemsFormType || DEFAULT_SETTINGS.ha_itemsFormType),
|
|
1432
1404
|
ha_itemsFormSlug: String(req.body.ha_itemsFormSlug || DEFAULT_SETTINGS.ha_itemsFormSlug),
|
|
1433
1405
|
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
/* global window, document, FullCalendar, bootbox */
|
|
3
3
|
|
|
4
4
|
(function () {
|
|
5
|
+
function populateItemsSelect() {
|
|
6
|
+
const sel = document.getElementById('ec-item-ids');
|
|
7
|
+
if (!sel) return;
|
|
8
|
+
const items = Array.isArray(window.EC_ITEMS) ? window.EC_ITEMS : [];
|
|
9
|
+
sel.innerHTML = '';
|
|
10
|
+
items.forEach((it) => {
|
|
11
|
+
if (!it || it.active === false) return;
|
|
12
|
+
const opt = document.createElement('option');
|
|
13
|
+
opt.value = String(it.id);
|
|
14
|
+
const p = parseFloat(it.price || '0') || 0;
|
|
15
|
+
const label = (Number.isInteger(p) ? String(p) : p.toFixed(2)) + ' €';
|
|
16
|
+
opt.textContent = `${it.name} — ${label}`;
|
|
17
|
+
opt.setAttribute('data-price', String(p));
|
|
18
|
+
sel.appendChild(opt);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
|
|
6
23
|
function toUtcMidnightMs(dateObj) {
|
|
7
24
|
return Date.UTC(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
|
|
@@ -171,19 +188,25 @@
|
|
|
171
188
|
|
|
172
189
|
const events = window.EC_EVENTS || [];
|
|
173
190
|
const initialDate = window.EC_INITIAL_DATE;
|
|
174
|
-
const initialView =
|
|
191
|
+
const initialView = 'dayGridMonth';
|
|
175
192
|
|
|
176
193
|
const calendar = new FullCalendar.Calendar(el, {
|
|
177
194
|
initialView: initialView,
|
|
178
195
|
initialDate: initialDate,
|
|
179
196
|
timeZone: window.EC_TZ || 'local',
|
|
180
197
|
selectable: window.EC_CAN_CREATE === true,
|
|
198
|
+
allDaySlot: true,
|
|
199
|
+
slotDuration: '24:00',
|
|
200
|
+
eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
|
|
181
201
|
selectMirror: true,
|
|
182
202
|
events: events,
|
|
203
|
+
displayEventTime: false,
|
|
183
204
|
|
|
184
205
|
// Range selection (drag)
|
|
185
|
-
select: function
|
|
186
|
-
|
|
206
|
+
select: function(info) {
|
|
207
|
+
const startMs = toUtcMidnightMs(info.start);
|
|
208
|
+
const endMs = toUtcMidnightMs(info.end);
|
|
209
|
+
openCreateModal(startMs, endMs);
|
|
187
210
|
},
|
|
188
211
|
|
|
189
212
|
// Single date click
|
|
@@ -193,13 +216,10 @@
|
|
|
193
216
|
openCreateModal(startMs, endMs);
|
|
194
217
|
},
|
|
195
218
|
|
|
196
|
-
headerToolbar: {
|
|
197
|
-
left: 'prev,next today',
|
|
198
|
-
center: 'title',
|
|
199
|
-
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
|
200
|
-
},
|
|
219
|
+
headerToolbar: { left: 'prev,next today', center: 'title', right: '' },
|
|
201
220
|
});
|
|
202
221
|
|
|
222
|
+
populateItemsSelect();
|
|
203
223
|
calendar.render();
|
|
204
224
|
}
|
|
205
225
|
|
|
@@ -33,23 +33,9 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
|
+
|
|
36
37
|
<div class="card card-body mb-3">
|
|
37
|
-
<h5>
|
|
38
|
-
|
|
39
|
-
<div class="mb-3">
|
|
40
|
-
<label class="form-label">Source du matériel</label>
|
|
41
|
-
<select class="form-select" name="itemsSource">
|
|
42
|
-
<option value="manual" {{{ if view_itemsSourceManual }}}selected{{{ end }}}>Manuel (JSON)</option>
|
|
43
|
-
<option value="helloasso" {{{ if view_itemsSourceHelloasso }}}selected{{{ end }}}>HelloAsso (catalogue)</option>
|
|
44
|
-
</select>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<div class="mb-3">
|
|
48
|
-
<label class="form-label">Matériel (JSON) — utilisé si Source = Manuel</label>
|
|
49
|
-
<textarea class="form-control" name="itemsJson" rows="8">{settings.itemsJson}</textarea>
|
|
50
|
-
<div class="form-text">Format: <code>[{"id":"cam1","name":"Caméra","price":50,"active":true}]</code> — <code>price</code> en euros (unitaire / jour).</div>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
38
|
+
<h5>Paiement</h5>
|
|
53
39
|
<div class="mb-0">
|
|
54
40
|
<label class="form-label">Timeout paiement (minutes)</label>
|
|
55
41
|
<input class="form-control" type="number" min="1" name="paymentTimeoutMinutes" value="{settings.paymentTimeoutMinutes}">
|
|
@@ -57,7 +43,7 @@
|
|
|
57
43
|
</div>
|
|
58
44
|
</div>
|
|
59
45
|
|
|
60
|
-
|
|
46
|
+
<div class="card card-body mb-3">
|
|
61
47
|
<h5>HelloAsso</h5>
|
|
62
48
|
|
|
63
49
|
<div class="mb-3">
|
|
@@ -13,14 +13,60 @@
|
|
|
13
13
|
<div id="equipment-calendar"></div>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
<!--
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
<!-- Modal création -->
|
|
17
|
+
<div class="modal fade" id="ec-create-modal" tabindex="-1" aria-hidden="true">
|
|
18
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
19
|
+
<div class="modal-content">
|
|
20
|
+
<div class="modal-header">
|
|
21
|
+
<h5 class="modal-title">Nouvelle demande</h5>
|
|
22
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<form id="ec-create-form" method="post" action="/equipment/reservations/create">
|
|
26
|
+
<div class="modal-body">
|
|
27
|
+
<input type="hidden" name="_csrf" value="{config.csrf_token}">
|
|
28
|
+
<input type="hidden" id="ec-start-ms" name="startMs" value="0">
|
|
29
|
+
<input type="hidden" id="ec-end-ms" name="endMs" value="0">
|
|
30
|
+
|
|
31
|
+
<div class="row g-3 mb-3">
|
|
32
|
+
<div class="col-6">
|
|
33
|
+
<label class="form-label">Début</label>
|
|
34
|
+
<input class="form-control" type="date" id="ec-start-date" required>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="col-6">
|
|
37
|
+
<label class="form-label">Fin</label>
|
|
38
|
+
<input class="form-control" type="date" id="ec-end-date" required>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="mb-3">
|
|
43
|
+
<label class="form-label">Matériel</label>
|
|
44
|
+
<select class="form-select" id="ec-item-ids" name="itemIds" multiple required></select>
|
|
45
|
+
<div class="form-text">Tu peux sélectionner plusieurs matériels.</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="mb-3">
|
|
49
|
+
<div class="fw-semibold">Durée</div>
|
|
50
|
+
<div id="ec-total-days">1 jour</div>
|
|
51
|
+
<hr class="my-2">
|
|
52
|
+
<div class="fw-semibold">Total estimé</div>
|
|
53
|
+
<div id="ec-total-price" class="fs-5">0 €</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="mb-0">
|
|
57
|
+
<label class="form-label">Note</label>
|
|
58
|
+
<textarea class="form-control" name="notesUser" rows="3" placeholder="Optionnel"></textarea>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="modal-footer">
|
|
63
|
+
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annuler</button>
|
|
64
|
+
<button type="submit" class="btn btn-primary">Envoyer la demande</button>
|
|
65
|
+
</div>
|
|
66
|
+
</form>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
24
70
|
</div>
|
|
25
71
|
|
|
26
72
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js"></script>
|
|
@@ -31,12 +77,6 @@
|
|
|
31
77
|
window.EC_BLOCKS = JSON.parse(atob('{blocksB64}'));
|
|
32
78
|
window.EC_ITEMS = JSON.parse(atob('{itemsB64}'));
|
|
33
79
|
window.EC_INITIAL_DATE = "{initialDateISO}";
|
|
34
|
-
window.EC_INITIAL_VIEW = "{view}";
|
|
35
80
|
window.EC_TZ = "{tz}";
|
|
36
81
|
window.EC_CAN_CREATE = {canCreateJs};
|
|
37
82
|
</script>
|
|
38
|
-
|
|
39
|
-
<style>
|
|
40
|
-
.ec-status-pending .fc-event-title { font-weight: 600; }
|
|
41
|
-
.ec-status-valid .fc-event-title { font-weight: 700; }
|
|
42
|
-
</style>
|