nodebb-plugin-equipment-calendar 9.1.9 → 10.0.0
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 +12 -84
- package/package.json +1 -1
- package/plugin.json +4 -1
- package/public/js/admin.js +97 -0
- package/templates/admin/plugins/equipment-calendar.tpl +64 -73
- package/templates/equipment-calendar/calendar.tpl +6 -104
package/library.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const meta = require.main.require('./src/meta');
|
|
4
4
|
const db = require.main.require('./src/database');
|
|
5
|
-
const groups = require.main.require('./src/groups');
|
|
6
5
|
|
|
6
|
+
const plugin = {};
|
|
7
7
|
const SETTINGS_KEY = 'equipmentCalendar';
|
|
8
8
|
|
|
9
9
|
const DEFAULT_SETTINGS = {
|
|
@@ -29,40 +29,9 @@ async function getSettings() {
|
|
|
29
29
|
return s;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
async function saveSettingsFromBody(body) {
|
|
33
|
-
const current = await getSettings();
|
|
34
|
-
const next = Object.assign({}, current, body || {});
|
|
35
|
-
next.paymentTimeoutMinutes = parseInt(next.paymentTimeoutMinutes, 10) || DEFAULT_SETTINGS.paymentTimeoutMinutes;
|
|
36
|
-
next.defaultView = next.defaultView || DEFAULT_SETTINGS.defaultView;
|
|
37
|
-
await meta.settings.set(SETTINGS_KEY, next);
|
|
38
|
-
return next;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function isUserInAnyGroups(uid, groupCsv) {
|
|
42
|
-
if (!uid || !groupCsv) return false;
|
|
43
|
-
const names = String(groupCsv).split(',').map(s => s.trim()).filter(Boolean);
|
|
44
|
-
for (const name of names) {
|
|
45
|
-
if (await groups.isMember(uid, name)) return true;
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function genRid() {
|
|
51
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
52
|
-
const r = Math.random() * 16 | 0;
|
|
53
|
-
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
54
|
-
return v.toString(16);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
32
|
function reservationKey(rid) { return `equipmentCalendar:reservation:${rid}`; }
|
|
59
33
|
const reservationsZset = 'equipmentCalendar:reservations';
|
|
60
34
|
|
|
61
|
-
async function saveReservation(r) {
|
|
62
|
-
await db.setObject(reservationKey(r.rid), r);
|
|
63
|
-
await db.sortedSetAdd(reservationsZset, r.createdAt || Date.now(), r.rid);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
35
|
async function listReservations(limit=1000) {
|
|
67
36
|
const rids = await db.getSortedSetRevRange(reservationsZset, 0, limit - 1);
|
|
68
37
|
const objs = await Promise.all(rids.map(id => db.getObject(reservationKey(id))));
|
|
@@ -70,75 +39,34 @@ async function listReservations(limit=1000) {
|
|
|
70
39
|
}
|
|
71
40
|
|
|
72
41
|
function toEvent(r) {
|
|
73
|
-
const start = String(r.startIso).slice(0,10);
|
|
74
|
-
const end =
|
|
75
|
-
|
|
76
|
-
const endExcl = end.toISOString().slice(0,10);
|
|
77
|
-
const status = r.status || 'pending';
|
|
78
|
-
const title = status === 'paid' ? '[PAID] Reservation' : (status === 'approved' ? '[APPROVED] Reservation' : '[PENDING] Reservation');
|
|
79
|
-
return { id: r.rid, title, start, end: endExcl, allDay: true };
|
|
42
|
+
const start = String(r.startIso || '').slice(0,10);
|
|
43
|
+
const end = String(r.endIso || '').slice(0,10);
|
|
44
|
+
return { id: r.rid, title: '[PENDING] Reservation', start, end, allDay: true };
|
|
80
45
|
}
|
|
81
46
|
|
|
82
|
-
const plugin = {};
|
|
83
|
-
|
|
84
47
|
plugin.init = async function (params) {
|
|
85
48
|
const { router, middleware } = params;
|
|
86
49
|
|
|
87
50
|
router.get('/admin/plugins/equipment-calendar', middleware.admin.buildHeader, async (req, res) => {
|
|
88
|
-
|
|
89
|
-
res.render('admin/plugins/equipment-calendar', {
|
|
90
|
-
settings,
|
|
91
|
-
savedFlag: (req.query && req.query.saved === '1') ? 'true' : 'false',
|
|
92
|
-
selected_dayGridMonth: settings.defaultView === 'dayGridMonth' ? 'selected' : '',
|
|
93
|
-
selected_timeGridWeek: settings.defaultView === 'timeGridWeek' ? 'selected' : '',
|
|
94
|
-
selected_listWeek: settings.defaultView === 'listWeek' ? 'selected' : '',
|
|
95
|
-
});
|
|
51
|
+
res.render('admin/plugins/equipment-calendar', {});
|
|
96
52
|
});
|
|
97
|
-
// NodeBB ACP ajaxify expects this route for plugin pages
|
|
98
|
-
router.get('/api/admin/plugins/equipment-calendar', middleware.admin.checkPrivileges, async (req, res) => {
|
|
99
|
-
const settings = await getSettings();
|
|
100
|
-
res.json({ settings });
|
|
101
|
-
});
|
|
102
53
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
router.post('/admin/plugins/equipment-calendar', middleware.admin.checkPrivileges, middleware.applyCSRF, async (req, res) => {
|
|
106
|
-
await saveSettingsFromBody(req.body || {});
|
|
107
|
-
res.redirect(`${req.baseUrl}${req.path}?saved=1`);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
router.get('/equipment/calendar', middleware.buildHeader, async (req, res) => {
|
|
54
|
+
router.get('/api/admin/plugins/equipment-calendar', middleware.admin.checkPrivileges, async (req, res) => {
|
|
111
55
|
const settings = await getSettings();
|
|
112
|
-
res.
|
|
56
|
+
res.json({ settings });
|
|
113
57
|
});
|
|
114
|
-
|
|
58
|
+
|
|
59
|
+
const renderCal = async (req, res) => {
|
|
115
60
|
const settings = await getSettings();
|
|
116
61
|
res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
|
|
117
|
-
}
|
|
62
|
+
};
|
|
63
|
+
router.get('/calendar', middleware.buildHeader, renderCal);
|
|
64
|
+
router.get('/equipment/calendar', middleware.buildHeader, renderCal);
|
|
118
65
|
|
|
119
66
|
router.get('/api/plugins/equipment-calendar/events', async (req, res) => {
|
|
120
67
|
const list = await listReservations(1000);
|
|
121
68
|
res.json({ events: list.map(toEvent) });
|
|
122
69
|
});
|
|
123
|
-
|
|
124
|
-
router.post('/api/plugins/equipment-calendar/reservations', middleware.applyCSRF, async (req, res) => {
|
|
125
|
-
const uid = req.uid;
|
|
126
|
-
if (!uid) return res.status(403).json({ message: 'forbidden' });
|
|
127
|
-
|
|
128
|
-
const settings = await getSettings();
|
|
129
|
-
const canCreate = await isUserInAnyGroups(uid, settings.creatorGroups) || await groups.isMember(uid, 'administrators');
|
|
130
|
-
if (!canCreate) return res.status(403).json({ message: 'forbidden' });
|
|
131
|
-
|
|
132
|
-
const startIso = String(req.body.startIso || '').slice(0,10);
|
|
133
|
-
const endIso = String(req.body.endIso || '').slice(0,10);
|
|
134
|
-
const itemIds = Array.isArray(req.body.itemIds) ? req.body.itemIds : [];
|
|
135
|
-
if (!startIso || !endIso) return res.status(400).json({ message: 'dates required' });
|
|
136
|
-
if (!itemIds.length) return res.status(400).json({ message: 'items required' });
|
|
137
|
-
|
|
138
|
-
const r = { rid: genRid(), uid, startIso, endIso, itemIds: itemIds.map(String), status: 'pending', createdAt: Date.now() };
|
|
139
|
-
await saveReservation(r);
|
|
140
|
-
res.json({ ok: true, rid: r.rid });
|
|
141
|
-
});
|
|
142
70
|
};
|
|
143
71
|
|
|
144
72
|
plugin.addAdminMenu = async function (header) {
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* globals ajaxify, socket, app */
|
|
3
|
+
|
|
4
|
+
(function () {
|
|
5
|
+
const HASH = 'equipmentCalendar';
|
|
6
|
+
|
|
7
|
+
function isOnPage() {
|
|
8
|
+
try {
|
|
9
|
+
const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : '';
|
|
10
|
+
return url.startsWith('admin/plugins/equipment-calendar');
|
|
11
|
+
} catch (e) { return false; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function rootEl() {
|
|
15
|
+
return document.querySelector('.equipment-calendar-settings');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fields() {
|
|
19
|
+
return Array.from(document.querySelectorAll('.equipment-calendar-settings [data-field]'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setValues(values) {
|
|
23
|
+
const map = values || {};
|
|
24
|
+
fields().forEach((el) => {
|
|
25
|
+
const key = el.getAttribute('data-field');
|
|
26
|
+
let val = map[key];
|
|
27
|
+
if (val === undefined || val === null) val = '';
|
|
28
|
+
el.value = String(val);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getValues() {
|
|
33
|
+
const out = {};
|
|
34
|
+
fields().forEach((el) => {
|
|
35
|
+
const key = el.getAttribute('data-field');
|
|
36
|
+
out[key] = el.value;
|
|
37
|
+
});
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function alertSuccess(msg) {
|
|
42
|
+
if (window.app && typeof app.alertSuccess === 'function') return app.alertSuccess(msg);
|
|
43
|
+
console.log('[success]', msg);
|
|
44
|
+
}
|
|
45
|
+
function alertError(msg) {
|
|
46
|
+
if (window.app && typeof app.alertError === 'function') return app.alertError(msg);
|
|
47
|
+
console.error('[error]', msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function socketEmit(name, payload) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
if (!window.socket) return reject(new Error('socket not available'));
|
|
53
|
+
socket.emit(name, payload, (err, data) => {
|
|
54
|
+
if (err) return reject(err);
|
|
55
|
+
resolve(data);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function load() {
|
|
61
|
+
const data = await socketEmit('admin.settings.get', { hash: HASH });
|
|
62
|
+
const values = (data && (data.values || data)) || {};
|
|
63
|
+
setValues(values);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function save() {
|
|
67
|
+
const values = getValues();
|
|
68
|
+
await socketEmit('admin.settings.save', { hash: HASH, values });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function init() {
|
|
72
|
+
if (!isOnPage()) return;
|
|
73
|
+
if (!rootEl()) return;
|
|
74
|
+
|
|
75
|
+
try { await load(); } catch (e) {}
|
|
76
|
+
|
|
77
|
+
const btn = document.getElementById('ec-save');
|
|
78
|
+
if (btn && !btn.__bound) {
|
|
79
|
+
btn.__bound = true;
|
|
80
|
+
btn.addEventListener('click', async (e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
try {
|
|
83
|
+
await save();
|
|
84
|
+
alertSuccess('[[admin/settings:settings-saved]]');
|
|
85
|
+
} catch (err) {
|
|
86
|
+
alertError(err && err.message ? err.message : String(err));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (window.jQuery) {
|
|
93
|
+
window.jQuery(window).on('action:ajaxify.end', init);
|
|
94
|
+
} else {
|
|
95
|
+
window.addEventListener('load', init);
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
@@ -1,86 +1,77 @@
|
|
|
1
|
-
<div class="acp-page-container">
|
|
1
|
+
<div class="acp-page-container equipment-calendar-settings">
|
|
2
2
|
<h1 class="mb-3">Equipment Calendar</h1>
|
|
3
3
|
|
|
4
|
-
<
|
|
5
|
-
|
|
4
|
+
<div class="alert alert-info mb-3">
|
|
5
|
+
Sauvegarde via l’API Socket NodeBB (comme les plugins officiels).
|
|
6
|
+
</div>
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
<div class="card card-body mb-3">
|
|
9
|
+
<h5 class="mb-3">Permissions</h5>
|
|
10
|
+
<div class="mb-3">
|
|
11
|
+
<label class="form-label">Groupes autorisés à créer (creatorGroups, séparés par virgule)</label>
|
|
12
|
+
<input class="form-control" type="text" data-field="creatorGroups">
|
|
13
|
+
</div>
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label class="form-label">Groupe valideur (approverGroup)</label>
|
|
16
|
+
<input class="form-control" type="text" data-field="approverGroup">
|
|
17
|
+
</div>
|
|
18
|
+
<div class="mb-0">
|
|
19
|
+
<label class="form-label">Groupe notifié (notifyGroup)</label>
|
|
20
|
+
<input class="form-control" type="text" data-field="notifyGroup">
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="card card-body mb-3">
|
|
25
|
+
<h5 class="mb-3">HelloAsso</h5>
|
|
26
|
+
<div class="row g-3">
|
|
27
|
+
<div class="col-md-6">
|
|
28
|
+
<label class="form-label">API Base URL (prod/sandbox)</label>
|
|
29
|
+
<input class="form-control" type="text" data-field="ha_apiBaseUrl" placeholder="https://api.helloasso.com ou https://api.helloasso-sandbox.com">
|
|
12
30
|
</div>
|
|
13
|
-
<div class="
|
|
14
|
-
<label class="form-label">
|
|
15
|
-
<input class="form-control" type="text"
|
|
31
|
+
<div class="col-md-6">
|
|
32
|
+
<label class="form-label">Organization slug</label>
|
|
33
|
+
<input class="form-control" type="text" data-field="ha_organizationSlug">
|
|
16
34
|
</div>
|
|
17
|
-
<div class="
|
|
18
|
-
<label class="form-label">
|
|
19
|
-
<input class="form-control" type="text"
|
|
35
|
+
<div class="col-md-6">
|
|
36
|
+
<label class="form-label">Client ID</label>
|
|
37
|
+
<input class="form-control" type="text" data-field="ha_clientId">
|
|
20
38
|
</div>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<div class="
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
<input class="form-control" type="text" name="ha_clientId" value="{settings.ha_clientId}">
|
|
37
|
-
</div>
|
|
38
|
-
<div class="col-md-6">
|
|
39
|
-
<label class="form-label">Client Secret</label>
|
|
40
|
-
<input class="form-control" type="password" name="ha_clientSecret" value="{settings.ha_clientSecret}">
|
|
41
|
-
</div>
|
|
42
|
-
<div class="col-md-6">
|
|
43
|
-
<label class="form-label">Form Type</label>
|
|
44
|
-
<input class="form-control" type="text" name="ha_itemsFormType" value="{settings.ha_itemsFormType}" placeholder="shop">
|
|
45
|
-
</div>
|
|
46
|
-
<div class="col-md-6">
|
|
47
|
-
<label class="form-label">Form Slug</label>
|
|
48
|
-
<input class="form-control" type="text" name="ha_itemsFormSlug" value="{settings.ha_itemsFormSlug}" placeholder="locations-materiel-2026">
|
|
49
|
-
</div>
|
|
50
|
-
<div class="col-12">
|
|
51
|
-
<label class="form-label">Return URL base (callback)</label>
|
|
52
|
-
<input class="form-control" type="text" name="ha_returnUrl" value="{settings.ha_returnUrl}" placeholder="https://www.onekite.com">
|
|
53
|
-
</div>
|
|
39
|
+
<div class="col-md-6">
|
|
40
|
+
<label class="form-label">Client Secret</label>
|
|
41
|
+
<input class="form-control" type="password" data-field="ha_clientSecret">
|
|
42
|
+
</div>
|
|
43
|
+
<div class="col-md-6">
|
|
44
|
+
<label class="form-label">Form Type</label>
|
|
45
|
+
<input class="form-control" type="text" data-field="ha_itemsFormType" placeholder="shop">
|
|
46
|
+
</div>
|
|
47
|
+
<div class="col-md-6">
|
|
48
|
+
<label class="form-label">Form Slug</label>
|
|
49
|
+
<input class="form-control" type="text" data-field="ha_itemsFormSlug" placeholder="locations-materiel-2026">
|
|
50
|
+
</div>
|
|
51
|
+
<div class="col-12">
|
|
52
|
+
<label class="form-label">Return URL base (callback)</label>
|
|
53
|
+
<input class="form-control" type="text" data-field="ha_returnUrl" placeholder="https://www.onekite.com">
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
|
+
</div>
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
</div>
|
|
58
|
+
<div class="card card-body mb-3">
|
|
59
|
+
<h5 class="mb-3">Calendrier</h5>
|
|
60
|
+
<div class="row g-3">
|
|
61
|
+
<div class="col-md-6">
|
|
62
|
+
<label class="form-label">Timeout paiement (minutes)</label>
|
|
63
|
+
<input class="form-control" type="number" min="1" data-field="paymentTimeoutMinutes">
|
|
64
|
+
</div>
|
|
65
|
+
<div class="col-md-6">
|
|
66
|
+
<label class="form-label">Vue par défaut</label>
|
|
67
|
+
<select class="form-select" data-field="defaultView">
|
|
68
|
+
<option value="dayGridMonth">Mois</option>
|
|
69
|
+
<option value="timeGridWeek">Semaine</option>
|
|
70
|
+
<option value="listWeek">Liste</option>
|
|
71
|
+
</select>
|
|
72
72
|
</div>
|
|
73
73
|
</div>
|
|
74
|
+
</div>
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
</form>
|
|
76
|
+
<button id="ec-save" class="btn btn-primary" type="button">Enregistrer</button>
|
|
77
77
|
</div>
|
|
78
|
-
|
|
79
|
-
<script>
|
|
80
|
-
(function () {
|
|
81
|
-
var saved = {savedFlag};
|
|
82
|
-
if (saved && window.app && typeof window.app.alertSuccess === 'function') {
|
|
83
|
-
window.app.alertSuccess('[[admin/settings:settings-saved]]');
|
|
84
|
-
}
|
|
85
|
-
})();
|
|
86
|
-
</script>
|
|
@@ -1,83 +1,25 @@
|
|
|
1
1
|
<div class="container-fluid px-3 equipment-calendar-page">
|
|
2
2
|
<h1 class="mb-3">Calendrier des réservations</h1>
|
|
3
|
-
|
|
4
3
|
<div id="ec-calendar"></div>
|
|
5
|
-
|
|
6
|
-
<div class="modal fade" id="ecModal" tabindex="-1" aria-hidden="true">
|
|
7
|
-
<div class="modal-dialog modal-dialog-centered">
|
|
8
|
-
<div class="modal-content">
|
|
9
|
-
<form id="ec-form">
|
|
10
|
-
<div class="modal-header">
|
|
11
|
-
<h5 class="modal-title">Nouvelle demande</h5>
|
|
12
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
|
13
|
-
</div>
|
|
14
|
-
<div class="modal-body">
|
|
15
|
-
<input type="hidden" name="_csrf" value="{csrf_token}">
|
|
16
|
-
<div class="mb-3">
|
|
17
|
-
<label class="form-label">Du</label>
|
|
18
|
-
<input class="form-control" type="date" id="ec-start" required>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="mb-3">
|
|
21
|
-
<label class="form-label">Au</label>
|
|
22
|
-
<input class="form-control" type="date" id="ec-end" required>
|
|
23
|
-
</div>
|
|
24
|
-
<div class="mb-3">
|
|
25
|
-
<label class="form-label">Matériel (IDs, séparés par virgule)</label>
|
|
26
|
-
<input class="form-control" type="text" id="ec-items" placeholder="ex: item123,item456" required>
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
<div class="modal-footer">
|
|
30
|
-
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Annuler</button>
|
|
31
|
-
<button class="btn btn-primary" type="submit">Réserver</button>
|
|
32
|
-
</div>
|
|
33
|
-
</form>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
4
|
</div>
|
|
38
5
|
|
|
39
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.css">
|
|
40
|
-
<script src="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js"></script>
|
|
6
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.css">
|
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
|
|
41
8
|
|
|
42
9
|
<script>
|
|
43
10
|
(function () {
|
|
44
|
-
function isoDate(s) { return String(s || '').slice(0, 10); }
|
|
45
|
-
function alertSuccess(msg){ if (window.app && app.alertSuccess) app.alertSuccess(msg); }
|
|
46
|
-
function alertError(msg){ if (window.app && app.alertError) app.alertError(msg); else console.error(msg); }
|
|
47
|
-
|
|
48
11
|
async function apiGet(path) {
|
|
49
12
|
var res = await fetch(config.relative_path + path, { credentials: 'same-origin' });
|
|
50
13
|
if (!res.ok) throw new Error('API error');
|
|
51
14
|
return res.json();
|
|
52
15
|
}
|
|
53
|
-
async function apiPost(path, body, csrf) {
|
|
54
|
-
var res = await fetch(config.relative_path + path, {
|
|
55
|
-
method: 'POST',
|
|
56
|
-
credentials: 'same-origin',
|
|
57
|
-
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf || '' },
|
|
58
|
-
body: JSON.stringify(body || {}),
|
|
59
|
-
});
|
|
60
|
-
var data = {};
|
|
61
|
-
try { data = await res.json(); } catch (e) {}
|
|
62
|
-
if (!res.ok) throw new Error(data.message || 'Erreur');
|
|
63
|
-
return data;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function openModal(startIso, endIso) {
|
|
67
|
-
document.getElementById('ec-start').value = startIso;
|
|
68
|
-
document.getElementById('ec-end').value = endIso;
|
|
69
|
-
var modalEl = document.getElementById('ecModal');
|
|
70
|
-
if (window.bootstrap && modalEl) {
|
|
71
|
-
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
16
|
|
|
75
17
|
async function init() {
|
|
76
18
|
var el = document.getElementById('ec-calendar');
|
|
77
19
|
if (!el) return;
|
|
78
20
|
|
|
79
21
|
if (!window.FullCalendar || !window.FullCalendar.Calendar) {
|
|
80
|
-
el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN
|
|
22
|
+
el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN bloqué ?).</div>';
|
|
81
23
|
return;
|
|
82
24
|
}
|
|
83
25
|
|
|
@@ -88,54 +30,14 @@
|
|
|
88
30
|
initialView: '{defaultView}',
|
|
89
31
|
headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,listWeek' },
|
|
90
32
|
selectable: true,
|
|
91
|
-
selectMirror: true,
|
|
92
33
|
displayEventTime: false,
|
|
93
|
-
select: function (info) {
|
|
94
|
-
var start = isoDate(info.startStr);
|
|
95
|
-
var endExcl = isoDate(info.endStr);
|
|
96
|
-
var end = endExcl;
|
|
97
|
-
if (endExcl) {
|
|
98
|
-
var d = new Date(endExcl + 'T00:00:00Z');
|
|
99
|
-
d.setUTCDate(d.getUTCDate() - 1);
|
|
100
|
-
end = d.toISOString().slice(0, 10);
|
|
101
|
-
}
|
|
102
|
-
openModal(start, end);
|
|
103
|
-
calendar.unselect();
|
|
104
|
-
},
|
|
105
|
-
dateClick: function (info) {
|
|
106
|
-
var d = isoDate(info.dateStr);
|
|
107
|
-
openModal(d, d);
|
|
108
|
-
},
|
|
109
34
|
events: events,
|
|
110
35
|
});
|
|
111
36
|
calendar.render();
|
|
112
|
-
|
|
113
|
-
var form = document.getElementById('ec-form');
|
|
114
|
-
if (form && !form.__bound) {
|
|
115
|
-
form.__bound = true;
|
|
116
|
-
form.addEventListener('submit', async function (e) {
|
|
117
|
-
e.preventDefault();
|
|
118
|
-
var startIso = document.getElementById('ec-start').value;
|
|
119
|
-
var endIso = document.getElementById('ec-end').value;
|
|
120
|
-
var itemIds = String(document.getElementById('ec-items').value || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
121
|
-
var csrf = form.querySelector('input[name="_csrf"]').value;
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
await apiPost('/api/plugins/equipment-calendar/reservations', { startIso: startIso, endIso: endIso, itemIds: itemIds }, csrf);
|
|
125
|
-
alertSuccess('Demande envoyée');
|
|
126
|
-
var modalEl = document.getElementById('ecModal');
|
|
127
|
-
if (window.bootstrap && modalEl) window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
|
|
128
|
-
|
|
129
|
-
var data2 = await apiGet('/api/plugins/equipment-calendar/events');
|
|
130
|
-
calendar.removeAllEvents();
|
|
131
|
-
(data2.events || []).forEach(function (ev) { calendar.addEvent(ev); });
|
|
132
|
-
} catch (err) {
|
|
133
|
-
alertError(err.message || 'Erreur');
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
37
|
}
|
|
138
38
|
|
|
139
|
-
init().catch(function () {
|
|
39
|
+
init().catch(function (e) {
|
|
40
|
+
console.error(e);
|
|
41
|
+
});
|
|
140
42
|
})();
|
|
141
43
|
</script>
|