nodebb-plugin-equipment-calendar 9.1.7 → 9.1.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 +30 -26
- package/package.json +1 -1
- package/plugin.json +0 -6
- package/templates/admin/plugins/equipment-calendar.tpl +73 -60
- package/templates/equipment-calendar/calendar.tpl +105 -3
- package/public/js/acp.js +0 -48
- package/public/js/admin.js +0 -109
- package/public/js/client.js +0 -130
package/library.js
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
const meta = require.main.require('./src/meta');
|
|
4
4
|
const db = require.main.require('./src/database');
|
|
5
5
|
const groups = require.main.require('./src/groups');
|
|
6
|
-
const winston = require.main.require('winston');
|
|
7
6
|
|
|
8
|
-
const plugin = {};
|
|
9
7
|
const SETTINGS_KEY = 'equipmentCalendar';
|
|
10
8
|
|
|
11
9
|
const DEFAULT_SETTINGS = {
|
|
@@ -27,14 +25,15 @@ async function getSettings() {
|
|
|
27
25
|
const raw = await meta.settings.get(SETTINGS_KEY);
|
|
28
26
|
const s = Object.assign({}, DEFAULT_SETTINGS, raw || {});
|
|
29
27
|
s.paymentTimeoutMinutes = parseInt(s.paymentTimeoutMinutes, 10) || DEFAULT_SETTINGS.paymentTimeoutMinutes;
|
|
28
|
+
s.defaultView = s.defaultView || DEFAULT_SETTINGS.defaultView;
|
|
30
29
|
return s;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
async function
|
|
32
|
+
async function saveSettingsFromBody(body) {
|
|
34
33
|
const current = await getSettings();
|
|
35
|
-
const next = Object.assign({}, current,
|
|
36
|
-
// Normalize
|
|
34
|
+
const next = Object.assign({}, current, body || {});
|
|
37
35
|
next.paymentTimeoutMinutes = parseInt(next.paymentTimeoutMinutes, 10) || DEFAULT_SETTINGS.paymentTimeoutMinutes;
|
|
36
|
+
next.defaultView = next.defaultView || DEFAULT_SETTINGS.defaultView;
|
|
38
37
|
await meta.settings.set(SETTINGS_KEY, next);
|
|
39
38
|
return next;
|
|
40
39
|
}
|
|
@@ -75,46 +74,53 @@ function toEvent(r) {
|
|
|
75
74
|
const end = new Date(String(r.endIso).slice(0,10) + 'T00:00:00Z');
|
|
76
75
|
end.setUTCDate(end.getUTCDate() + 1);
|
|
77
76
|
const endExcl = end.toISOString().slice(0,10);
|
|
78
|
-
|
|
79
77
|
const status = r.status || 'pending';
|
|
80
78
|
const title = status === 'paid' ? '[PAID] Reservation' : (status === 'approved' ? '[APPROVED] Reservation' : '[PENDING] Reservation');
|
|
81
79
|
return { id: r.rid, title, start, end: endExcl, allDay: true };
|
|
82
80
|
}
|
|
83
81
|
|
|
82
|
+
const plugin = {};
|
|
83
|
+
|
|
84
84
|
plugin.init = async function (params) {
|
|
85
85
|
const { router, middleware } = params;
|
|
86
86
|
|
|
87
|
-
// ACP page
|
|
88
87
|
router.get('/admin/plugins/equipment-calendar', middleware.admin.buildHeader, async (req, res) => {
|
|
89
|
-
res.render('admin/plugins/equipment-calendar', {});
|
|
90
|
-
});
|
|
91
|
-
router.get('/api/admin/plugins/equipment-calendar', async (req, res) => res.json({}));
|
|
92
|
-
|
|
93
|
-
// ACP settings API (official-style behaviour without requirejs)
|
|
94
|
-
router.get('/api/admin/plugins/equipment-calendar/settings', middleware.admin.checkPrivileges, async (req, res) => {
|
|
95
88
|
const settings = await getSettings();
|
|
96
|
-
res.
|
|
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
|
+
});
|
|
97
96
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
|
|
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`);
|
|
101
108
|
});
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
async function renderCal(req, res) {
|
|
110
|
+
router.get('/equipment/calendar', middleware.buildHeader, async (req, res) => {
|
|
105
111
|
const settings = await getSettings();
|
|
106
112
|
res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
|
|
107
|
-
}
|
|
108
|
-
router.get('/
|
|
109
|
-
|
|
113
|
+
});
|
|
114
|
+
router.get('/calendar', middleware.buildHeader, async (req, res) => {
|
|
115
|
+
const settings = await getSettings();
|
|
116
|
+
res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
|
|
117
|
+
});
|
|
110
118
|
|
|
111
|
-
// Events
|
|
112
119
|
router.get('/api/plugins/equipment-calendar/events', async (req, res) => {
|
|
113
120
|
const list = await listReservations(1000);
|
|
114
121
|
res.json({ events: list.map(toEvent) });
|
|
115
122
|
});
|
|
116
123
|
|
|
117
|
-
// Create reservation
|
|
118
124
|
router.post('/api/plugins/equipment-calendar/reservations', middleware.applyCSRF, async (req, res) => {
|
|
119
125
|
const uid = req.uid;
|
|
120
126
|
if (!uid) return res.status(403).json({ message: 'forbidden' });
|
|
@@ -133,8 +139,6 @@ plugin.init = async function (params) {
|
|
|
133
139
|
await saveReservation(r);
|
|
134
140
|
res.json({ ok: true, rid: r.rid });
|
|
135
141
|
});
|
|
136
|
-
|
|
137
|
-
winston.info('[equipment-calendar] loaded (NodeBB 4.x no-require)');
|
|
138
142
|
};
|
|
139
143
|
|
|
140
144
|
plugin.addAdminMenu = async function (header) {
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,73 +1,86 @@
|
|
|
1
|
-
<div class="acp-page-container
|
|
1
|
+
<div class="acp-page-container">
|
|
2
2
|
<h1 class="mb-3">Equipment Calendar</h1>
|
|
3
3
|
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
<div class="mb-3">
|
|
7
|
-
<label class="form-label">Groupes autorisés à créer (creatorGroups, séparés par virgule)</label>
|
|
8
|
-
<input class="form-control" type="text" id="creatorGroups">
|
|
9
|
-
</div>
|
|
10
|
-
<div class="mb-3">
|
|
11
|
-
<label class="form-label">Groupe valideur (approverGroup)</label>
|
|
12
|
-
<input class="form-control" type="text" id="approverGroup">
|
|
13
|
-
</div>
|
|
14
|
-
<div class="mb-0">
|
|
15
|
-
<label class="form-label">Groupe notifié (notifyGroup)</label>
|
|
16
|
-
<input class="form-control" type="text" id="notifyGroup">
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
4
|
+
<form method="post" action="{config.relative_path}/admin/plugins/equipment-calendar">
|
|
5
|
+
<input type="hidden" name="_csrf" value="{csrf_token}">
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
<input class="form-control" type="text" id="ha_apiBaseUrl" placeholder="https://api.helloasso.com ou https://api.helloasso-sandbox.com">
|
|
26
|
-
</div>
|
|
27
|
-
<div class="col-md-6">
|
|
28
|
-
<label class="form-label">Organization slug</label>
|
|
29
|
-
<input class="form-control" type="text" id="ha_organizationSlug">
|
|
30
|
-
</div>
|
|
31
|
-
<div class="col-md-6">
|
|
32
|
-
<label class="form-label">Client ID</label>
|
|
33
|
-
<input class="form-control" type="text" id="ha_clientId">
|
|
7
|
+
<div class="card card-body mb-3">
|
|
8
|
+
<h5 class="mb-3">Permissions</h5>
|
|
9
|
+
<div class="mb-3">
|
|
10
|
+
<label class="form-label">Groupes autorisés à créer (creatorGroups, séparés par virgule)</label>
|
|
11
|
+
<input class="form-control" type="text" name="creatorGroups" value="{settings.creatorGroups}">
|
|
34
12
|
</div>
|
|
35
|
-
<div class="
|
|
36
|
-
<label class="form-label">
|
|
37
|
-
<input class="form-control" type="
|
|
13
|
+
<div class="mb-3">
|
|
14
|
+
<label class="form-label">Groupe valideur (approverGroup)</label>
|
|
15
|
+
<input class="form-control" type="text" name="approverGroup" value="{settings.approverGroup}">
|
|
38
16
|
</div>
|
|
39
|
-
<div class="
|
|
40
|
-
<label class="form-label">
|
|
41
|
-
<input class="form-control" type="text"
|
|
42
|
-
</div>
|
|
43
|
-
<div class="col-md-6">
|
|
44
|
-
<label class="form-label">Form Slug</label>
|
|
45
|
-
<input class="form-control" type="text" id="ha_itemsFormSlug" placeholder="locations-materiel-2026">
|
|
46
|
-
</div>
|
|
47
|
-
<div class="col-12">
|
|
48
|
-
<label class="form-label">Return URL base (callback)</label>
|
|
49
|
-
<input class="form-control" type="text" id="ha_returnUrl" placeholder="https://www.onekite.com">
|
|
17
|
+
<div class="mb-0">
|
|
18
|
+
<label class="form-label">Groupe notifié (notifyGroup)</label>
|
|
19
|
+
<input class="form-control" type="text" name="notifyGroup" value="{settings.notifyGroup}">
|
|
50
20
|
</div>
|
|
51
21
|
</div>
|
|
52
|
-
</div>
|
|
53
22
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
23
|
+
<div class="card card-body mb-3">
|
|
24
|
+
<h5 class="mb-3">HelloAsso</h5>
|
|
25
|
+
<div class="row g-3">
|
|
26
|
+
<div class="col-md-6">
|
|
27
|
+
<label class="form-label">API Base URL (prod/sandbox)</label>
|
|
28
|
+
<input class="form-control" type="text" name="ha_apiBaseUrl" value="{settings.ha_apiBaseUrl}" placeholder="https://api.helloasso.com ou https://api.helloasso-sandbox.com">
|
|
29
|
+
</div>
|
|
30
|
+
<div class="col-md-6">
|
|
31
|
+
<label class="form-label">Organization slug</label>
|
|
32
|
+
<input class="form-control" type="text" name="ha_organizationSlug" value="{settings.ha_organizationSlug}">
|
|
33
|
+
</div>
|
|
34
|
+
<div class="col-md-6">
|
|
35
|
+
<label class="form-label">Client ID</label>
|
|
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>
|
|
60
54
|
</div>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="card card-body mb-3">
|
|
58
|
+
<h5 class="mb-3">Calendrier</h5>
|
|
59
|
+
<div class="row g-3">
|
|
60
|
+
<div class="col-md-6">
|
|
61
|
+
<label class="form-label">Timeout paiement (minutes)</label>
|
|
62
|
+
<input class="form-control" type="number" min="1" name="paymentTimeoutMinutes" value="{settings.paymentTimeoutMinutes}">
|
|
63
|
+
</div>
|
|
64
|
+
<div class="col-md-6">
|
|
65
|
+
<label class="form-label">Vue par défaut</label>
|
|
66
|
+
<select class="form-select" name="defaultView">
|
|
67
|
+
<option value="dayGridMonth" {selected_dayGridMonth}>Mois</option>
|
|
68
|
+
<option value="timeGridWeek" {selected_timeGridWeek}>Semaine</option>
|
|
69
|
+
<option value="listWeek" {selected_listWeek}>Liste</option>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
68
72
|
</div>
|
|
69
73
|
</div>
|
|
70
|
-
</div>
|
|
71
74
|
|
|
72
|
-
|
|
75
|
+
<button class="btn btn-primary" type="submit">Enregistrer</button>
|
|
76
|
+
</form>
|
|
73
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,5 +1,6 @@
|
|
|
1
1
|
<div class="container-fluid px-3 equipment-calendar-page">
|
|
2
2
|
<h1 class="mb-3">Calendrier des réservations</h1>
|
|
3
|
+
|
|
3
4
|
<div id="ec-calendar"></div>
|
|
4
5
|
|
|
5
6
|
<div class="modal fade" id="ecModal" tabindex="-1" aria-hidden="true">
|
|
@@ -14,15 +15,15 @@
|
|
|
14
15
|
<input type="hidden" name="_csrf" value="{csrf_token}">
|
|
15
16
|
<div class="mb-3">
|
|
16
17
|
<label class="form-label">Du</label>
|
|
17
|
-
<input class="form-control" type="date"
|
|
18
|
+
<input class="form-control" type="date" id="ec-start" required>
|
|
18
19
|
</div>
|
|
19
20
|
<div class="mb-3">
|
|
20
21
|
<label class="form-label">Au</label>
|
|
21
|
-
<input class="form-control" type="date"
|
|
22
|
+
<input class="form-control" type="date" id="ec-end" required>
|
|
22
23
|
</div>
|
|
23
24
|
<div class="mb-3">
|
|
24
25
|
<label class="form-label">Matériel (IDs, séparés par virgule)</label>
|
|
25
|
-
<input class="form-control" type="text"
|
|
26
|
+
<input class="form-control" type="text" id="ec-items" placeholder="ex: item123,item456" required>
|
|
26
27
|
</div>
|
|
27
28
|
</div>
|
|
28
29
|
<div class="modal-footer">
|
|
@@ -37,3 +38,104 @@
|
|
|
37
38
|
|
|
38
39
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.css">
|
|
39
40
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js"></script>
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
(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
|
+
async function apiGet(path) {
|
|
49
|
+
var res = await fetch(config.relative_path + path, { credentials: 'same-origin' });
|
|
50
|
+
if (!res.ok) throw new Error('API error');
|
|
51
|
+
return res.json();
|
|
52
|
+
}
|
|
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
|
+
|
|
75
|
+
async function init() {
|
|
76
|
+
var el = document.getElementById('ec-calendar');
|
|
77
|
+
if (!el) return;
|
|
78
|
+
|
|
79
|
+
if (!window.FullCalendar || !window.FullCalendar.Calendar) {
|
|
80
|
+
el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN). Vérifie la CSP et l’accès à cdn.jsdelivr.net.</div>';
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var data = await apiGet('/api/plugins/equipment-calendar/events');
|
|
85
|
+
var events = (data && data.events) ? data.events : [];
|
|
86
|
+
|
|
87
|
+
var calendar = new window.FullCalendar.Calendar(el, {
|
|
88
|
+
initialView: '{defaultView}',
|
|
89
|
+
headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,listWeek' },
|
|
90
|
+
selectable: true,
|
|
91
|
+
selectMirror: true,
|
|
92
|
+
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
|
+
events: events,
|
|
110
|
+
});
|
|
111
|
+
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
|
+
}
|
|
138
|
+
|
|
139
|
+
init().catch(function () {});
|
|
140
|
+
})();
|
|
141
|
+
</script>
|
package/public/js/acp.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/* globals $, ajaxify, require */
|
|
3
|
-
|
|
4
|
-
(function () {
|
|
5
|
-
function onEnd(Settings, alerts) {
|
|
6
|
-
const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : (window.location.pathname || '');
|
|
7
|
-
if (!url.startsWith('admin/plugins/equipment-calendar')) return;
|
|
8
|
-
|
|
9
|
-
const container = $('.equipment-calendar-settings');
|
|
10
|
-
if (!container.length) return;
|
|
11
|
-
|
|
12
|
-
// load
|
|
13
|
-
try {
|
|
14
|
-
if (typeof Settings.sync === 'function') {
|
|
15
|
-
Settings.sync('equipmentCalendar', container);
|
|
16
|
-
} else {
|
|
17
|
-
Settings.load('equipmentCalendar', container);
|
|
18
|
-
}
|
|
19
|
-
} catch (e) {}
|
|
20
|
-
|
|
21
|
-
$('#ec-save').off('click.ec').on('click.ec', function (e) {
|
|
22
|
-
e.preventDefault();
|
|
23
|
-
const done = function () {
|
|
24
|
-
alerts.success('[[admin/settings:settings-saved]]');
|
|
25
|
-
};
|
|
26
|
-
try {
|
|
27
|
-
if (typeof Settings.persist === 'function') {
|
|
28
|
-
Settings.persist('equipmentCalendar', container, done, true);
|
|
29
|
-
} else {
|
|
30
|
-
Settings.save('equipmentCalendar', container, done);
|
|
31
|
-
}
|
|
32
|
-
} catch (err) {
|
|
33
|
-
alerts.error(err && err.message ? err.message : 'Erreur');
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
$(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', function () {
|
|
39
|
-
require(['settings', 'alerts'], function (Settings, alerts) {
|
|
40
|
-
onEnd(Settings, alerts);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// run once
|
|
45
|
-
require(['settings', 'alerts'], function (Settings, alerts) {
|
|
46
|
-
onEnd(Settings, alerts);
|
|
47
|
-
});
|
|
48
|
-
})();
|
package/public/js/admin.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/* globals ajaxify, app */
|
|
3
|
-
|
|
4
|
-
(function () {
|
|
5
|
-
function alertSuccess(msg) {
|
|
6
|
-
if (window.app && typeof window.app.alertSuccess === 'function') return window.app.alertSuccess(msg);
|
|
7
|
-
// fallback: simple
|
|
8
|
-
console.log('[success]', msg);
|
|
9
|
-
}
|
|
10
|
-
function alertError(msg) {
|
|
11
|
-
if (window.app && typeof window.app.alertError === 'function') return window.app.alertError(msg);
|
|
12
|
-
console.error('[error]', msg);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isAcpPage() {
|
|
16
|
-
const path = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : '';
|
|
17
|
-
return path.startsWith('admin/plugins/equipment-calendar');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function csrfToken() {
|
|
21
|
-
// NodeBB exposes csrf_token in ajaxify.data when on ACP pages
|
|
22
|
-
return (ajaxify && ajaxify.data && ajaxify.data.csrf_token) ? ajaxify.data.csrf_token : '';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function q(id) { return document.getElementById(id); }
|
|
26
|
-
|
|
27
|
-
function readForm() {
|
|
28
|
-
return {
|
|
29
|
-
creatorGroups: q('creatorGroups')?.value || '',
|
|
30
|
-
approverGroup: q('approverGroup')?.value || '',
|
|
31
|
-
notifyGroup: q('notifyGroup')?.value || '',
|
|
32
|
-
ha_apiBaseUrl: q('ha_apiBaseUrl')?.value || '',
|
|
33
|
-
ha_organizationSlug: q('ha_organizationSlug')?.value || '',
|
|
34
|
-
ha_clientId: q('ha_clientId')?.value || '',
|
|
35
|
-
ha_clientSecret: q('ha_clientSecret')?.value || '',
|
|
36
|
-
ha_itemsFormType: q('ha_itemsFormType')?.value || '',
|
|
37
|
-
ha_itemsFormSlug: q('ha_itemsFormSlug')?.value || '',
|
|
38
|
-
ha_returnUrl: q('ha_returnUrl')?.value || '',
|
|
39
|
-
paymentTimeoutMinutes: q('paymentTimeoutMinutes')?.value || '',
|
|
40
|
-
defaultView: q('defaultView')?.value || '',
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function fillForm(s) {
|
|
45
|
-
if (!s) return;
|
|
46
|
-
const set = (id, v) => { const el = q(id); if (el) el.value = v ?? ''; };
|
|
47
|
-
set('creatorGroups', s.creatorGroups);
|
|
48
|
-
set('approverGroup', s.approverGroup);
|
|
49
|
-
set('notifyGroup', s.notifyGroup);
|
|
50
|
-
set('ha_apiBaseUrl', s.ha_apiBaseUrl);
|
|
51
|
-
set('ha_organizationSlug', s.ha_organizationSlug);
|
|
52
|
-
set('ha_clientId', s.ha_clientId);
|
|
53
|
-
set('ha_clientSecret', s.ha_clientSecret);
|
|
54
|
-
set('ha_itemsFormType', s.ha_itemsFormType);
|
|
55
|
-
set('ha_itemsFormSlug', s.ha_itemsFormSlug);
|
|
56
|
-
set('ha_returnUrl', s.ha_returnUrl);
|
|
57
|
-
set('paymentTimeoutMinutes', s.paymentTimeoutMinutes);
|
|
58
|
-
set('defaultView', s.defaultView);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function loadSettings() {
|
|
62
|
-
const res = await fetch(`${config.relative_path}/api/admin/plugins/equipment-calendar/settings`, { credentials: 'same-origin' });
|
|
63
|
-
const data = await res.json();
|
|
64
|
-
fillForm(data.settings);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function saveSettings() {
|
|
68
|
-
const payload = readForm();
|
|
69
|
-
const res = await fetch(`${config.relative_path}/api/admin/plugins/equipment-calendar/settings`, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
credentials: 'same-origin',
|
|
72
|
-
headers: {
|
|
73
|
-
'Content-Type': 'application/json',
|
|
74
|
-
'x-csrf-token': csrfToken(),
|
|
75
|
-
},
|
|
76
|
-
body: JSON.stringify(payload),
|
|
77
|
-
});
|
|
78
|
-
const data = await res.json().catch(() => ({}));
|
|
79
|
-
if (!res.ok) throw new Error(data.message || 'Save failed');
|
|
80
|
-
alertSuccess('[[admin/settings:settings-saved]]');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function init() {
|
|
84
|
-
if (!isAcpPage()) return;
|
|
85
|
-
if (!document.querySelector('.equipment-calendar-settings')) return;
|
|
86
|
-
|
|
87
|
-
try { await loadSettings(); } catch (e) { /* ignore */ }
|
|
88
|
-
|
|
89
|
-
const btn = q('ec-save');
|
|
90
|
-
if (btn) {
|
|
91
|
-
btn.addEventListener('click', async (e) => {
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
try { await saveSettings(); } catch (err) { alertError(err.message || 'Erreur'); }
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Run on initial load
|
|
99
|
-
if (document.readyState === 'loading') {
|
|
100
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
101
|
-
} else {
|
|
102
|
-
init();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Also run after ajaxify navigation if available
|
|
106
|
-
if (window.jQuery) {
|
|
107
|
-
window.jQuery(window).on('action:ajaxify.end', init);
|
|
108
|
-
}
|
|
109
|
-
})();
|
package/public/js/client.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/* globals ajaxify */
|
|
3
|
-
|
|
4
|
-
(function () {
|
|
5
|
-
function pageMatch() {
|
|
6
|
-
const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : '';
|
|
7
|
-
return url === 'equipment/calendar' || url === 'calendar';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function isoDate(s) { return String(s || '').slice(0, 10); }
|
|
11
|
-
|
|
12
|
-
async function apiGet(path) {
|
|
13
|
-
const res = await fetch(`${config.relative_path}${path}`, { credentials: 'same-origin' });
|
|
14
|
-
if (!res.ok) throw new Error('API error');
|
|
15
|
-
return res.json();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function apiPost(path, body, csrf) {
|
|
19
|
-
const res = await fetch(`${config.relative_path}${path}`, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
credentials: 'same-origin',
|
|
22
|
-
headers: {
|
|
23
|
-
'Content-Type': 'application/json',
|
|
24
|
-
'x-csrf-token': csrf || '',
|
|
25
|
-
},
|
|
26
|
-
body: JSON.stringify(body),
|
|
27
|
-
});
|
|
28
|
-
const data = await res.json().catch(() => ({}));
|
|
29
|
-
if (!res.ok) throw new Error(data.message || 'Erreur');
|
|
30
|
-
return data;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function csrfTokenFromForm() {
|
|
34
|
-
const el = document.querySelector('#ec-form input[name="_csrf"]');
|
|
35
|
-
return el ? el.value : '';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function openModal(startIso, endIso) {
|
|
39
|
-
const start = document.getElementById('ec-start');
|
|
40
|
-
const end = document.getElementById('ec-end');
|
|
41
|
-
if (start) start.value = startIso;
|
|
42
|
-
if (end) end.value = endIso;
|
|
43
|
-
|
|
44
|
-
const modalEl = document.getElementById('ecModal');
|
|
45
|
-
if (!modalEl || !window.bootstrap) return;
|
|
46
|
-
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function initCalendar() {
|
|
50
|
-
if (!pageMatch()) return;
|
|
51
|
-
const el = document.getElementById('ec-calendar');
|
|
52
|
-
if (!el) return;
|
|
53
|
-
|
|
54
|
-
if (!window.FullCalendar || !window.FullCalendar.Calendar) {
|
|
55
|
-
el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN). Vérifie la CSP et l’accès à cdn.jsdelivr.net.</div>';
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const data = await apiGet('/api/plugins/equipment-calendar/events');
|
|
60
|
-
const events = (data && data.events) ? data.events : [];
|
|
61
|
-
|
|
62
|
-
const calendar = new window.FullCalendar.Calendar(el, {
|
|
63
|
-
initialView: 'dayGridMonth',
|
|
64
|
-
headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,listWeek' },
|
|
65
|
-
selectable: true,
|
|
66
|
-
selectMirror: true,
|
|
67
|
-
displayEventTime: false,
|
|
68
|
-
select: function (info) {
|
|
69
|
-
const start = isoDate(info.startStr);
|
|
70
|
-
const endExcl = isoDate(info.endStr);
|
|
71
|
-
let end = endExcl;
|
|
72
|
-
if (endExcl) {
|
|
73
|
-
const d = new Date(endExcl + 'T00:00:00Z');
|
|
74
|
-
d.setUTCDate(d.getUTCDate() - 1);
|
|
75
|
-
end = d.toISOString().slice(0, 10);
|
|
76
|
-
}
|
|
77
|
-
openModal(start, end);
|
|
78
|
-
calendar.unselect();
|
|
79
|
-
},
|
|
80
|
-
dateClick: function (info) {
|
|
81
|
-
const d = isoDate(info.dateStr);
|
|
82
|
-
openModal(d, d);
|
|
83
|
-
},
|
|
84
|
-
events,
|
|
85
|
-
});
|
|
86
|
-
calendar.render();
|
|
87
|
-
|
|
88
|
-
const form = document.getElementById('ec-form');
|
|
89
|
-
if (form) {
|
|
90
|
-
form.addEventListener('submit', async (e) => {
|
|
91
|
-
e.preventDefault();
|
|
92
|
-
const startIso = document.getElementById('ec-start')?.value || '';
|
|
93
|
-
const endIso = document.getElementById('ec-end')?.value || '';
|
|
94
|
-
const itemIds = String(document.getElementById('ec-items')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
95
|
-
const csrf = csrfTokenFromForm();
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
await apiPost('/api/plugins/equipment-calendar/reservations', { startIso, endIso, itemIds }, csrf);
|
|
99
|
-
|
|
100
|
-
if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
101
|
-
window.app.alertSuccess('Demande envoyée');
|
|
102
|
-
}
|
|
103
|
-
const modalEl = document.getElementById('ecModal');
|
|
104
|
-
if (modalEl && window.bootstrap) window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
|
|
105
|
-
|
|
106
|
-
const data2 = await apiGet('/api/plugins/equipment-calendar/events');
|
|
107
|
-
calendar.removeAllEvents();
|
|
108
|
-
(data2.events || []).forEach(ev => calendar.addEvent(ev));
|
|
109
|
-
} catch (err) {
|
|
110
|
-
if (window.app && typeof window.app.alertError === 'function') {
|
|
111
|
-
window.app.alertError(err.message || 'Erreur');
|
|
112
|
-
} else {
|
|
113
|
-
alert(err.message || 'Erreur');
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}, { once: true });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function init() { initCalendar().catch(() => {}); }
|
|
121
|
-
|
|
122
|
-
if (document.readyState === 'loading') {
|
|
123
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
124
|
-
} else {
|
|
125
|
-
init();
|
|
126
|
-
}
|
|
127
|
-
if (window.jQuery) {
|
|
128
|
-
window.jQuery(window).on('action:ajaxify.end', init);
|
|
129
|
-
}
|
|
130
|
-
})();
|