nodebb-plugin-calendar-onekite 1.4.7 → 2.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/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "1.4.7",
4
- "description": "",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://git.worobel.net/Arnaud/nodebb-plugin-calendar-onekite.git"
8
- },
9
- "license": "ISC",
10
- "author": "",
11
- "type": "commonjs",
3
+ "version": "2.0.0",
4
+ "description": "Calendar + equipment booking + admin approval + HelloAsso payments for NodeBB v4 (no-jQuery UI)",
12
5
  "main": "library.js",
13
- "nbbpm": {
14
- "compatibility": ">=4.0.0 <5.0.0"
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "nodebb",
9
+ "plugin",
10
+ "calendar",
11
+ "booking",
12
+ "helloasso",
13
+ "fullcalendar"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
15
17
  },
16
- "files": [
17
- "plugin.json",
18
- "library.js",
19
- "helloasso.js",
20
- "templates/",
21
- "static/"
22
- ]
18
+ "dependencies": {},
19
+ "scripts": {
20
+ "lint": "echo \"(optional) add eslint if you want\"",
21
+ "test": "echo \"no tests\""
22
+ }
23
23
  }
package/plugin.json CHANGED
@@ -1,21 +1,33 @@
1
- {
2
- "id": "nodebb-plugin-calendar-onekite",
3
- "name": "Calendar Onekite",
4
- "description": "Calendrier + réservation de matériel + validation admin + paiement HelloAsso pour NodeBB v4",
5
- "url": "",
6
- "version": "1.4.6",
7
- "library": "./library.js",
8
- "staticDirs": {
9
- "static": "static"
10
- },
11
- "hooks": [
12
- { "hook": "static:app.load", "method": "init" },
13
- { "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
14
- { "hook": "filter:widgets.getWidgets", "method": "defineWidgets" },
15
- { "hook": "filter:widget.render:calendarUpcoming", "method": "renderUpcomingWidget" }
16
- ],
17
- "acpScripts": [
18
- "static/js/admin.js"
19
- ],
20
- "templates": "templates"
21
- }
1
+ {
2
+ "id": "nodebb-plugin-calendar-onekite",
3
+ "name": "Calendar Onekite",
4
+ "description": "Calendrier + réservation matériel + validation admin + paiement HelloAsso pour NodeBB v4 (no-jQuery UI)",
5
+ "url": "",
6
+ "version": "2.0.0",
7
+ "library": "./library.js",
8
+ "staticDirs": {
9
+ "static": "static"
10
+ },
11
+ "acpScripts": [
12
+ "static/js/admin.js"
13
+ ],
14
+ "hooks": [
15
+ {
16
+ "hook": "static:app.load",
17
+ "method": "init"
18
+ },
19
+ {
20
+ "hook": "filter:admin.header.build",
21
+ "method": "addAdminNavigation"
22
+ },
23
+ {
24
+ "hook": "filter:widgets.getWidgets",
25
+ "method": "defineWidgets"
26
+ },
27
+ {
28
+ "hook": "filter:widget.render:calendarUpcoming",
29
+ "method": "renderUpcomingWidget"
30
+ }
31
+ ],
32
+ "templates": "templates"
33
+ }
@@ -0,0 +1,4 @@
1
+ .calendar-modal{display:none;position:fixed;z-index:9999;left:0;top:0;width:100%;height:100%;overflow:auto;background:rgba(0,0,0,.45)}
2
+ .calendar-modal-content{background:#fff;margin:5vh auto;padding:16px;width:min(900px,92vw);border-radius:10px;box-shadow:0 10px 40px rgba(0,0,0,.2)}
3
+ .calendar-close{float:right;cursor:pointer;font-size:22px;line-height:1}
4
+ #calendar{min-height:600px}
@@ -1,172 +1,184 @@
1
- define('plugins/nodebb-plugin-calendar-onekite/static/js/admin', [
2
- 'api',
3
- 'alerts',
4
- ], function (api, alerts) {
5
- 'use strict';
6
-
7
- const AdminCalendar = {};
8
-
9
- AdminCalendar.init = function () {
10
- AdminCalendar.initSettings();
11
- AdminCalendar.initPlanning();
12
- };
13
-
14
- AdminCalendar.initSettings = function () {
15
- if (!$('#calendar-onekite-admin').length) return;
16
- AdminCalendar.bindSettingsSave();
17
- AdminCalendar.loadPending();
18
- };
19
-
20
- AdminCalendar.initPlanning = function () {
21
- if (!$('#calendar-planning').length) return;
22
- AdminCalendar.loadPlanning();
23
- };
24
-
25
- AdminCalendar.bindSettingsSave = function () {
26
- // évite les doubles binds quand ajaxify recharge la page
27
- $('#calendar-onekite-save')
28
- .off('click.calendarOnekite')
29
- .on('click.calendarOnekite', function () {
30
- const settings = {
31
- allowedGroups: $('#calendar-onekite-groups').val(),
32
- allowedBookingGroups: $('#calendar-onekite-book-groups').val(),
33
- limit: $('#calendar-onekite-widget-limit').val(),
34
- };
35
-
36
- // ✅ endpoint standard (doit exister côté serveur)
37
- api.put('/admin/plugins/calendar-onekite', settings)
38
- .then(() => alerts.success('Paramètres enregistrés'))
39
- .catch(err => alerts.error(err?.message || err));
40
- });
41
- };
42
-
43
- AdminCalendar.loadPending = function () {
44
- api.get('/admin/calendar/pending')
45
- .then(list => {
46
- const container = $('#pending-reservations');
47
- if (!container.length) return;
48
-
49
- container.empty();
50
-
51
- if (!list.length) {
52
- container.html('<p>Aucune réservation en attente.</p>');
53
- return;
54
- }
55
-
56
- list.forEach(block => {
57
- const ev = block.event;
58
- const card = $(`
59
- <div class="card mb-3">
60
- <div class="card-header">
61
- <strong>${ev.title}</strong><br>
62
- <small>${ev.start} → ${ev.end}</small>
63
- </div>
64
- <div class="card-body"></div>
65
- </div>
66
- `);
67
-
68
- const body = card.find('.card-body');
69
-
70
- block.reservations.forEach(r => {
71
- const row = $(`
72
- <div class="reservation-row mb-2 p-2 border rounded">
73
- <p><strong>RID :</strong> ${r.rid}</p>
74
- <p><strong>UID :</strong> ${r.uid}</p>
75
- <p><strong>Matériel :</strong> ${r.itemId}</p>
76
- <p><strong>Quantité :</strong> ${r.quantity}</p>
77
- <p><strong>Dates :</strong> ${r.dateStart} → ${r.dateEnd} (${r.days || ''} jours)</p>
78
- <p><strong>Lieu de retrait :</strong> ${r.pickupLocation || 'Non précisé'}</p>
79
- <button class="btn btn-success btn-sm validate-btn">Valider</button>
80
- <button class="btn btn-danger btn-sm cancel-btn">Annuler</button>
81
- </div>
82
- `);
83
-
84
- row.find('.validate-btn').on('click', () => {
85
- AdminCalendar.validateReservation(r.rid, row);
86
- });
87
-
88
- row.find('.cancel-btn').on('click', () => {
89
- AdminCalendar.cancelReservation(r.rid, row);
90
- });
91
-
92
- body.append(row);
93
- });
94
-
95
- container.append(card);
96
- });
97
- })
98
- .catch(err => alerts.error(err?.message || err));
99
- };
100
-
101
- AdminCalendar.validateReservation = function (rid, row) {
102
- api.post('/admin/calendar/reservation/' + rid + '/validate', {})
103
- .then(() => {
104
- row.css('background', '#d4edda');
105
- row.find('.validate-btn').remove();
106
- row.find('.cancel-btn').remove();
107
- row.append('<p><strong>Validée.</strong> Lien de paiement envoyé.</p>');
108
- })
109
- .catch(err => alerts.error(err?.message || err));
110
- };
111
-
112
- AdminCalendar.cancelReservation = function (rid, row) {
113
- require(['bootbox'], function (bootbox) {
114
- bootbox.confirm('Annuler cette réservation ?', ok => {
115
- if (!ok) return;
116
- api.post('/admin/calendar/reservation/' + rid + '/cancel', {})
117
- .then(() => {
118
- row.css('background', '#f8d7da');
119
- row.find('.validate-btn').remove();
120
- row.find('.cancel-btn').remove();
121
- row.append('<p><strong>Annulée.</strong></p>');
122
- })
123
- .catch(err => alerts.error(err?.message || err));
124
- });
125
- });
126
- };
127
-
128
- AdminCalendar.loadPlanning = function () {
129
- api.get('/admin/calendar/planning')
130
- .then(rows => {
131
- const tbody = $('#planning-body');
132
- if (!tbody.length) return;
133
-
134
- tbody.empty();
135
-
136
- if (!rows.length) {
137
- tbody.append('<tr><td colspan="9">Aucune réservation future.</td></tr>');
138
- return;
139
- }
140
-
141
- rows.forEach(r => {
142
- const tr = $(`
143
- <tr>
144
- <td>${r.eventTitle}</td>
145
- <td>${r.itemName}</td>
146
- <td>${r.pickupLocation || 'Non précisé'}</td>
147
- <td>${r.uid}</td>
148
- <td>${r.quantity}</td>
149
- <td>${r.dateStart}</td>
150
- <td>${r.dateEnd}</td>
151
- <td>${r.days}</td>
152
- <td>${r.status}</td>
153
- </tr>
154
- `);
155
- tbody.append(tr);
156
- });
157
- })
158
- .catch(err => alerts.error(err?.message || err));
159
- };
160
-
161
- // essentiel : relance l’init à chaque navigation ACP ajaxify
162
- $(window).on('action:ajaxify.end', function () {
163
- AdminCalendar.init();
164
- });
165
-
166
- // et au chargement initial
167
- $(function () {
168
- AdminCalendar.init();
169
- });
170
-
171
- return AdminCalendar;
172
- });
1
+ define('plugins/nodebb-plugin-calendar-onekite/static/js/admin', ['api', 'alerts'], function (api, alerts) {
2
+ 'use strict';
3
+
4
+ const STATE = { initedSettings:false, initedPlanning:false, delegationBound:false };
5
+
6
+ function qs(sel, root=document){ return root.querySelector(sel); }
7
+ function qsa(sel, root=document){ return Array.from(root.querySelectorAll(sel)); }
8
+
9
+ function escapeHtml(s){
10
+ return String(s ?? '')
11
+ .replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
12
+ .replaceAll('"','&quot;').replaceAll("'","&#039;");
13
+ }
14
+
15
+ async function saveSettings() {
16
+ const settings = {
17
+ allowedGroups: qs('#calendar-onekite-groups')?.value || '',
18
+ allowedBookingGroups: qs('#calendar-onekite-book-groups')?.value || '',
19
+ limit: qs('#calendar-onekite-widget-limit')?.value || '',
20
+ helloassoApiBase: qs('#calendar-onekite-helloasso-apibase')?.value || '',
21
+ helloassoOrganizationSlug: qs('#calendar-onekite-helloasso-org')?.value || '',
22
+ helloassoFormSlug: qs('#calendar-onekite-helloasso-form')?.value || '',
23
+ helloassoClientId: qs('#calendar-onekite-helloasso-clientid')?.value || '',
24
+ helloassoClientSecret: qs('#calendar-onekite-helloasso-secret')?.value || '',
25
+ helloassoReturnUrl: qs('#calendar-onekite-helloasso-return')?.value || '',
26
+ };
27
+
28
+ try {
29
+ await api.put('/admin/plugins/calendar-onekite', settings);
30
+ alerts.success('Paramètres enregistrés');
31
+ } catch (e) {
32
+ alerts.error(e?.message || e);
33
+ }
34
+ }
35
+
36
+ function renderPending(container, list) {
37
+ if (!Array.isArray(list) || list.length === 0) {
38
+ container.innerHTML = '<p>Aucune réservation en attente.</p>';
39
+ return;
40
+ }
41
+
42
+ container.innerHTML = list.map(block => {
43
+ const ev = block.event || {};
44
+ const reservations = Array.isArray(block.reservations) ? block.reservations : [];
45
+ const rows = reservations.map(r => `
46
+ <div class="reservation-row mb-2 p-2 border rounded" data-rid="${escapeHtml(r.rid)}">
47
+ <p><strong>RID :</strong> ${escapeHtml(r.rid)}</p>
48
+ <p><strong>UID :</strong> ${escapeHtml(r.uid)}</p>
49
+ <p><strong>Matériel :</strong> ${escapeHtml(r.itemId)}</p>
50
+ <p><strong>Quantité :</strong> ${escapeHtml(r.quantity)}</p>
51
+ <p><strong>Dates :</strong> ${escapeHtml(r.dateStart)} → ${escapeHtml(r.dateEnd)} (${escapeHtml(r.days||'')} jours)</p>
52
+ <p><strong>Lieu de retrait :</strong> ${escapeHtml(r.pickupLocation || 'Non précisé')}</p>
53
+ <button class="btn btn-success btn-sm js-validate">Valider</button>
54
+ <button class="btn btn-danger btn-sm js-cancel">Annuler</button>
55
+ </div>
56
+ `).join('');
57
+
58
+ return `
59
+ <div class="card mb-3">
60
+ <div class="card-header">
61
+ <strong>${escapeHtml(ev.title)}</strong><br>
62
+ <small>${escapeHtml(ev.start)} → ${escapeHtml(ev.end)}</small>
63
+ </div>
64
+ <div class="card-body">${rows}</div>
65
+ </div>
66
+ `;
67
+ }).join('');
68
+ }
69
+
70
+ async function loadPending() {
71
+ const container = qs('#pending-reservations');
72
+ if (!container) return;
73
+ try {
74
+ const list = await api.get('/admin/calendar/pending');
75
+ renderPending(container, list);
76
+ } catch (e) {
77
+ alerts.error(e?.message || e);
78
+ }
79
+ }
80
+
81
+ async function validateReservation(rid, row) {
82
+ try {
83
+ await api.post(`/admin/calendar/reservation/${encodeURIComponent(rid)}/validate`, {});
84
+ row.style.background = '#d4edda';
85
+ qsa('.js-validate, .js-cancel', row).forEach(b => b.remove());
86
+ row.insertAdjacentHTML('beforeend', '<p><strong>Validée.</strong> Lien de paiement envoyé.</p>');
87
+ } catch (e) {
88
+ alerts.error(e?.message || e);
89
+ }
90
+ }
91
+
92
+ async function cancelReservation(rid, row) {
93
+ if (!window.confirm('Annuler cette réservation ?')) return;
94
+ try {
95
+ await api.post(`/admin/calendar/reservation/${encodeURIComponent(rid)}/cancel`, {});
96
+ row.style.background = '#f8d7da';
97
+ qsa('.js-validate, .js-cancel', row).forEach(b => b.remove());
98
+ row.insertAdjacentHTML('beforeend', '<p><strong>Annulée.</strong></p>');
99
+ } catch (e) {
100
+ alerts.error(e?.message || e);
101
+ }
102
+ }
103
+
104
+ function bindDelegationOnce() {
105
+ if (STATE.delegationBound) return;
106
+ STATE.delegationBound = true;
107
+
108
+ document.addEventListener('click', (ev) => {
109
+ const t = ev.target;
110
+ if (!(t instanceof HTMLElement)) return;
111
+
112
+ const row = t.closest('.reservation-row');
113
+ if (!row) return;
114
+
115
+ const rid = row.getAttribute('data-rid');
116
+ if (!rid) return;
117
+
118
+ if (t.classList.contains('js-validate')) validateReservation(rid, row);
119
+ if (t.classList.contains('js-cancel')) cancelReservation(rid, row);
120
+ }, { passive: true });
121
+ }
122
+
123
+ async function loadPlanning() {
124
+ const tbody = qs('#planning-body');
125
+ if (!tbody) return;
126
+
127
+ try {
128
+ const rows = await api.get('/admin/calendar/planning');
129
+ if (!rows || !rows.length) {
130
+ tbody.innerHTML = '<tr><td colspan="9">Aucune réservation future.</td></tr>';
131
+ return;
132
+ }
133
+ tbody.innerHTML = rows.map(r => `
134
+ <tr>
135
+ <td>${escapeHtml(r.eventTitle)}</td>
136
+ <td>${escapeHtml(r.itemName)}</td>
137
+ <td>${escapeHtml(r.pickupLocation || 'Non précisé')}</td>
138
+ <td>${escapeHtml(r.uid)}</td>
139
+ <td>${escapeHtml(r.quantity)}</td>
140
+ <td>${escapeHtml(r.dateStart)}</td>
141
+ <td>${escapeHtml(r.dateEnd)}</td>
142
+ <td>${escapeHtml(r.days)}</td>
143
+ <td>${escapeHtml(r.status)}</td>
144
+ </tr>
145
+ `).join('');
146
+ } catch (e) {
147
+ alerts.error(e?.message || e);
148
+ }
149
+ }
150
+
151
+ function initSettingsPage() {
152
+ const root = qs('#calendar-onekite-admin');
153
+ if (!root || STATE.initedSettings) return;
154
+ STATE.initedSettings = true;
155
+
156
+ qs('#calendar-onekite-save')?.addEventListener('click', saveSettings);
157
+ bindDelegationOnce();
158
+ loadPending();
159
+ }
160
+
161
+ function initPlanningPage() {
162
+ const root = qs('#calendar-planning');
163
+ if (!root || STATE.initedPlanning) return;
164
+ STATE.initedPlanning = true;
165
+ loadPlanning();
166
+ }
167
+
168
+ function resetFlagsIfLeft() {
169
+ if (!qs('#calendar-onekite-admin')) STATE.initedSettings = false;
170
+ if (!qs('#calendar-planning')) STATE.initedPlanning = false;
171
+ }
172
+
173
+ const obs = new MutationObserver(() => {
174
+ resetFlagsIfLeft();
175
+ initSettingsPage();
176
+ initPlanningPage();
177
+ });
178
+ obs.observe(document.body, { childList: true, subtree: true });
179
+
180
+ initSettingsPage();
181
+ initPlanningPage();
182
+
183
+ return {};
184
+ });
@@ -0,0 +1,55 @@
1
+ define('plugins/nodebb-plugin-calendar-onekite/static/js/my-reservations', ['api', 'alerts'], function (api, alerts) {
2
+ 'use strict';
3
+ const STATE = { inited:false };
4
+
5
+ function qs(sel, root=document){ return root.querySelector(sel); }
6
+ function escapeHtml(s){
7
+ return String(s ?? '')
8
+ .replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
9
+ .replaceAll('"','&quot;').replaceAll("'","&#039;");
10
+ }
11
+
12
+ async function load() {
13
+ const root = qs('#calendar-my-reservations');
14
+ const tbody = qs('#my-reservations-body');
15
+ if (!root || !tbody) return;
16
+
17
+ try {
18
+ const rows = await api.get('/calendar/my-reservations');
19
+ if (!rows || !rows.length) {
20
+ tbody.innerHTML = '<tr><td colspan="7">Aucune réservation.</td></tr>';
21
+ return;
22
+ }
23
+ tbody.innerHTML = rows.map(r => `
24
+ <tr>
25
+ <td>${escapeHtml(r.eventTitle)}</td>
26
+ <td>${escapeHtml(r.itemName)}</td>
27
+ <td>${escapeHtml(r.pickupLocation || '')}</td>
28
+ <td>${escapeHtml(r.dateStart)}</td>
29
+ <td>${escapeHtml(r.dateEnd)}</td>
30
+ <td>${escapeHtml(r.days || '')}</td>
31
+ <td>${escapeHtml(r.status)}</td>
32
+ </tr>
33
+ `).join('');
34
+ } catch (e) {
35
+ alerts.error(e?.message || e);
36
+ }
37
+ }
38
+
39
+ function initOnce() {
40
+ if (STATE.inited) return;
41
+ if (!qs('#calendar-my-reservations')) return;
42
+ STATE.inited = true;
43
+ load();
44
+ }
45
+
46
+ function resetIfLeft() {
47
+ if (!qs('#calendar-my-reservations')) STATE.inited = false;
48
+ }
49
+
50
+ const obs = new MutationObserver(() => { resetIfLeft(); initOnce(); });
51
+ obs.observe(document.body, { childList:true, subtree:true });
52
+ initOnce();
53
+
54
+ return { init: initOnce };
55
+ });