nodebb-plugin-calendar-onekite 1.0.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/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "nodebb-plugin-calendar-onekite",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://git.worobel.net/Arnaud/nodebb-plugin-calendar-lite-onekite.git"
8
+ },
9
+ "license": "ISC",
10
+ "author": "",
11
+ "type": "commonjs",
12
+ "main": "library.js",
13
+ "nbbpm": {
14
+ "compatibility": "^4.0.0"
15
+ },
16
+ "files": [
17
+ "plugin.json",
18
+ "library.js",
19
+ "templates/",
20
+ "static/"
21
+ ]
22
+ }
package/plugin.json ADDED
@@ -0,0 +1,18 @@
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.0.1",
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
+ "templates": "templates"
18
+ }
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ $(document).ready(function () {
4
+ if (!$('#calendar-lite-admin').length && !$('#calendar-planning').length) return;
5
+
6
+ const AdminCalendar = {
7
+
8
+ initSettings: function () {
9
+ if (!$('#calendar-lite-admin').length) return;
10
+ AdminCalendar.bindSettingsSave();
11
+ AdminCalendar.loadPending();
12
+ },
13
+
14
+ initPlanning: function () {
15
+ if (!$('#calendar-planning').length) return;
16
+ AdminCalendar.loadPlanning();
17
+ },
18
+
19
+ bindSettingsSave: function () {
20
+ $('#calendar-lite-save').on('click', () => {
21
+ const settings = {
22
+ allowedGroups: $('#calendar-lite-groups').val(),
23
+ allowedBookingGroups: $('#calendar-lite-book-groups').val(),
24
+ limit: $('#calendar-lite-widget-limit').val()
25
+ };
26
+
27
+ $.ajax({
28
+ url: '/api/admin/plugins/calendar-lite',
29
+ method: 'PUT',
30
+ contentType: 'application/json',
31
+ data: JSON.stringify(settings)
32
+ })
33
+ .then(() => app.alertSuccess('Paramètres enregistrés'))
34
+ .catch(err => app.alertError(err.responseJSON?.error || err.message));
35
+ });
36
+ },
37
+
38
+ loadPending: function () {
39
+ $.get('/api/admin/calendar/pending').then(list => {
40
+ const container = $('#pending-reservations');
41
+ if (!container.length) return;
42
+
43
+ container.empty();
44
+
45
+ if (!list.length) {
46
+ container.html('<p>Aucune réservation en attente.</p>');
47
+ return;
48
+ }
49
+
50
+ list.forEach(block => {
51
+ const ev = block.event;
52
+ const card = $(`
53
+ <div class="card mb-3">
54
+ <div class="card-header">
55
+ <strong>${ev.title}</strong><br>
56
+ <small>${ev.start} → ${ev.end}</small>
57
+ </div>
58
+ <div class="card-body"></div>
59
+ </div>
60
+ `);
61
+
62
+ const body = card.find('.card-body');
63
+
64
+ block.reservations.forEach(r => {
65
+ const row = $(`
66
+ <div class="reservation-row mb-2 p-2 border rounded">
67
+ <p><strong>RID :</strong> ${r.rid}</p>
68
+ <p><strong>UID :</strong> ${r.uid}</p>
69
+ <p><strong>Matériel :</strong> ${r.itemId}</p>
70
+ <p><strong>Quantité :</strong> ${r.quantity}</p>
71
+ <p><strong>Dates :</strong> ${r.dateStart} → ${r.dateEnd} (${r.days || ''} jours)</p>
72
+ <p><strong>Lieu de retrait :</strong> ${r.pickupLocation || 'Non précisé'}</p>
73
+ <button class="btn btn-success btn-sm validate-btn">Valider</button>
74
+ <button class="btn btn-danger btn-sm cancel-btn">Annuler</button>
75
+ </div>
76
+ `);
77
+
78
+ row.find('.validate-btn').on('click', () => {
79
+ AdminCalendar.validateReservation(r.rid, row);
80
+ });
81
+
82
+ row.find('.cancel-btn').on('click', () => {
83
+ AdminCalendar.cancelReservation(r.rid, row);
84
+ });
85
+
86
+ body.append(row);
87
+ });
88
+
89
+ container.append(card);
90
+ });
91
+ });
92
+ },
93
+
94
+ validateReservation: function (rid, row) {
95
+ $.post('/api/admin/calendar/reservation/' + rid + '/validate')
96
+ .then(res => {
97
+ row.css('background', '#d4edda');
98
+ row.find('.validate-btn').remove();
99
+ row.find('.cancel-btn').remove();
100
+ row.append('<p><strong>Validée.</strong> Lien de paiement envoyé.</p>');
101
+ })
102
+ .catch(err => {
103
+ app.alertError(err.responseJSON?.error || err.message);
104
+ });
105
+ },
106
+
107
+ cancelReservation: function (rid, row) {
108
+ bootbox.confirm('Annuler cette réservation ?', ok => {
109
+ if (!ok) return;
110
+ $.post('/api/admin/calendar/reservation/' + rid + '/cancel')
111
+ .then(() => {
112
+ row.css('background', '#f8d7da');
113
+ row.find('.validate-btn').remove();
114
+ row.find('.cancel-btn').remove();
115
+ row.append('<p><strong>Annulée.</strong></p>');
116
+ })
117
+ .catch(err => app.alertError(err.responseJSON?.error || err.message));
118
+ });
119
+ },
120
+
121
+ loadPlanning: function () {
122
+ $.get('/api/admin/calendar/planning').then(rows => {
123
+ const tbody = $('#planning-body');
124
+ if (!tbody.length) return;
125
+
126
+ tbody.empty();
127
+
128
+ if (!rows.length) {
129
+ tbody.append('<tr><td colspan="8">Aucune réservation future.</td></tr>');
130
+ return;
131
+ }
132
+
133
+ rows.forEach(r => {
134
+ const tr = $(`
135
+ <tr>
136
+ <td>${r.eventTitle}</td>
137
+ <td>${r.itemName}</td>
138
+ <td>${r.pickupLocation || 'Non précisé'}</td>
139
+ <td>${r.uid}</td>
140
+ <td>${r.quantity}</td>
141
+ <td>${r.dateStart}</td>
142
+ <td>${r.dateEnd}</td>
143
+ <td>${r.days}</td>
144
+ <td>${r.status}</td>
145
+ </tr>
146
+ `);
147
+ tbody.append(tr);
148
+ });
149
+ });
150
+ }
151
+ };
152
+
153
+ AdminCalendar.initSettings();
154
+ AdminCalendar.initPlanning();
155
+ });
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ $(document).ready(function () {
4
+ if (!$('#calendar').length) return;
5
+
6
+ const CalendarPage = {
7
+
8
+ canBook: false,
9
+
10
+ init: function () {
11
+ this.prepareModals();
12
+ this.checkPermissions();
13
+ this.initCalendar();
14
+ },
15
+
16
+ checkPermissions: function () {
17
+ $.get('/api/calendar/permissions/book').then(res => {
18
+ CalendarPage.canBook = !!(res && res.allow);
19
+ });
20
+ },
21
+
22
+ prepareModals: function () {
23
+ $('.calendar-modal').hide();
24
+ $('.calendar-modal .calendar-close').on('click', function () {
25
+ $(this).closest('.calendar-modal').fadeOut();
26
+ });
27
+ },
28
+
29
+ initCalendar: function () {
30
+ const calendarEl = document.getElementById('calendar');
31
+
32
+ const calendar = new FullCalendar.Calendar(calendarEl, {
33
+ themeSystem: 'bootstrap5',
34
+ initialView: 'dayGridMonth',
35
+ locale: 'fr',
36
+ height: 'auto',
37
+ selectable: true,
38
+ headerToolbar: {
39
+ left: 'prev,next today',
40
+ center: 'title',
41
+ right: 'dayGridMonth,timeGridWeek,listWeek'
42
+ },
43
+
44
+ events: (info, success, fail) => {
45
+ $.get('/api/calendar/events', { start: info.startStr, end: info.endStr })
46
+ .then(events => {
47
+ success(events.map(ev => ({
48
+ id: ev.eid,
49
+ title: ev.title,
50
+ start: ev.start,
51
+ end: ev.end,
52
+ allDay: ev.allDay == 1,
53
+ extendedProps: ev
54
+ })));
55
+ })
56
+ .catch(fail);
57
+ },
58
+
59
+ dateClick: info => {
60
+ CalendarPage.openCreateEventModal(info.dateStr);
61
+ },
62
+
63
+ eventClick: info => {
64
+ CalendarPage.openEventModal(info.event.extendedProps);
65
+ }
66
+ });
67
+
68
+ calendar.render();
69
+
70
+ // bouton nouvel event
71
+ $.get('/api/calendar/permissions/create').then(res => {
72
+ if (!res || !res.allow) {
73
+ $('#calendar-new-event').hide();
74
+ } else {
75
+ $('#calendar-new-event').on('click', () => {
76
+ CalendarPage.openCreateEventModal();
77
+ });
78
+ }
79
+ });
80
+ },
81
+
82
+ /* -------- MODAL EVENT -------- */
83
+
84
+ openCreateEventModal: function (dateStr) {
85
+ const modal = $('#calendar-event-modal');
86
+
87
+ $('#event-modal-title').text('Créer un événement');
88
+ $('#event-title').val('');
89
+ $('#event-description').val('');
90
+ const base = dateStr || new Date().toISOString().slice(0, 10);
91
+ $('#event-start').val(base + 'T09:00');
92
+ $('#event-end').val(base + 'T11:00');
93
+ $('#event-allDay').prop('checked', false);
94
+ $('#event-location').val('');
95
+ $('#event-bookingEnabled').prop('checked', false);
96
+ $('#booking-items').empty();
97
+
98
+ $('#event-save').off('click').on('click', () => {
99
+ CalendarPage.saveEvent();
100
+ });
101
+
102
+ $('#event-delete').hide();
103
+ $('#event-reserve').hide();
104
+
105
+ $('#event-add-item').off('click').on('click', () => {
106
+ CalendarPage.addBookingItemRow();
107
+ });
108
+
109
+ modal.fadeIn();
110
+ },
111
+
112
+ openEventModal: function (eventData) {
113
+ const modal = $('#calendar-event-modal');
114
+
115
+ $('#event-modal-title').text('Événement : ' + eventData.title);
116
+ $('#event-title').val(eventData.title);
117
+ $('#event-description').val(eventData.description || '');
118
+ $('#event-start').val(eventData.start.replace('Z', ''));
119
+ $('#event-end').val(eventData.end.replace('Z', ''));
120
+ $('#event-allDay').prop('checked', eventData.allDay == 1);
121
+ $('#event-location').val(eventData.location || '');
122
+ $('#event-bookingEnabled').prop('checked', eventData.bookingEnabled == 1);
123
+
124
+ CalendarPage.renderBookingItems(eventData.bookingItems || []);
125
+
126
+ $('#event-add-item').off('click').on('click', () => {
127
+ CalendarPage.addBookingItemRow();
128
+ });
129
+
130
+ $('#event-save').off('click').on('click', () => {
131
+ CalendarPage.updateEvent(eventData.eid);
132
+ });
133
+
134
+ $('#event-delete').show().off('click').on('click', () => {
135
+ bootbox.confirm('Supprimer cet événement ?', ok => {
136
+ if (!ok) return;
137
+ $.ajax({ url: '/api/calendar/event/' + eventData.eid, method: 'DELETE' })
138
+ .then(() => location.reload());
139
+ });
140
+ });
141
+
142
+ if (CalendarPage.canBook && Number(eventData.bookingEnabled)) {
143
+ $('#event-reserve').show().off('click').on('click', () => {
144
+ CalendarPage.openReserveModal(eventData.eid);
145
+ });
146
+ } else {
147
+ $('#event-reserve').hide();
148
+ }
149
+
150
+ modal.fadeIn();
151
+ },
152
+
153
+ saveEvent: function () {
154
+ const data = CalendarPage.collectEventForm();
155
+ $.ajax({
156
+ url: '/api/calendar/event',
157
+ method: 'POST',
158
+ contentType: 'application/json',
159
+ data: JSON.stringify(data),
160
+ }).then(() => location.reload())
161
+ .catch(err => app.alertError(err.responseJSON?.error || err.message));
162
+ },
163
+
164
+ updateEvent: function (eid) {
165
+ const data = CalendarPage.collectEventForm();
166
+ $.ajax({
167
+ url: '/api/calendar/event/' + eid,
168
+ method: 'PUT',
169
+ contentType: 'application/json',
170
+ data: JSON.stringify(data),
171
+ }).then(() => location.reload())
172
+ .catch(err => app.alertError(err.responseJSON?.error || err.message));
173
+ },
174
+
175
+ collectEventForm: function () {
176
+ const items = [];
177
+ $('#booking-items .booking-item-row').each(function () {
178
+ const row = $(this);
179
+ const total = Number(row.find('.item-total').val() || 0);
180
+ if (!total) return;
181
+
182
+ items.push({
183
+ id: row.find('.item-id').val(),
184
+ name: row.find('.item-name').val(),
185
+ total,
186
+ reserved: Number(row.find('.item-reserved').val() || 0),
187
+ reservedTemp: Number(row.find('.item-reservedTemp').val() || 0),
188
+ price: Number(row.find('.item-price').val() || 0),
189
+ pickupLocation: row.find('.item-pickup').val() || ''
190
+ });
191
+ });
192
+
193
+ return {
194
+ title: $('#event-title').val(),
195
+ description: $('#event-description').val(),
196
+ start: $('#event-start').val(),
197
+ end: $('#event-end').val(),
198
+ allDay: $('#event-allDay').is(':checked'),
199
+ location: $('#event-location').val(),
200
+ bookingEnabled: $('#event-bookingEnabled').is(':checked'),
201
+ bookingItems: items
202
+ };
203
+ },
204
+
205
+ renderBookingItems: function (items) {
206
+ const container = $('#booking-items');
207
+ container.empty();
208
+ (items || []).forEach(item => CalendarPage.addBookingItemRow(item));
209
+ },
210
+
211
+ addBookingItemRow: function (item = {}) {
212
+ const row = $(`
213
+ <div class="booking-item-row">
214
+ <input type="text" class="item-id form-control" placeholder="ID" value="${item.id || ''}">
215
+ <input type="text" class="item-name form-control" placeholder="Nom matériel" value="${item.name || ''}">
216
+ <input type="number" class="item-total form-control" placeholder="Total" value="${item.total || 0}">
217
+ <input type="number" class="item-price form-control" placeholder="Prix €" step="0.01" value="${item.price || 0}">
218
+ <input type="text" class="item-pickup form-control" placeholder="Lieu retrait" value="${item.pickupLocation || ''}">
219
+ <input type="hidden" class="item-reserved" value="${item.reserved || 0}">
220
+ <input type="hidden" class="item-reservedTemp" value="${item.reservedTemp || 0}">
221
+ <button class="btn btn-danger btn-sm item-remove">✕</button>
222
+ </div>
223
+ `);
224
+
225
+ row.find('.item-remove').on('click', () => row.remove());
226
+ $('#booking-items').append(row);
227
+ },
228
+
229
+ /* -------- MODAL RÉSERVATION -------- */
230
+
231
+ openReserveModal: function (eid) {
232
+ $.get('/api/calendar/event/' + eid).then(eventData => {
233
+ const modal = $('#calendar-reserve-modal');
234
+
235
+ $('#reserve-title').text(eventData.title);
236
+
237
+ const items = eventData.bookingItems || [];
238
+ if (!items.length || !Number(eventData.bookingEnabled)) {
239
+ $('#reserve-items').html('<p>Aucun matériel réservable pour cet événement.</p>');
240
+ $('#reserve-confirm').hide();
241
+ } else {
242
+ let html = '<ul>';
243
+ items.forEach((item, index) => {
244
+ const pickup = item.pickupLocation || 'Lieu de retrait non précisé';
245
+ html += `
246
+ <li>
247
+ <label>
248
+ <input type="radio" name="reserve-item" value="${item.id}" ${index === 0 ? 'checked' : ''}>
249
+ <strong>${item.name}</strong>
250
+ ${item.price ? `– ${item.price} €/jour` : ''}
251
+ <br><small>📍 Retrait : ${pickup}</small>
252
+ </label>
253
+ </li>`;
254
+ });
255
+ html += '</ul>';
256
+ $('#reserve-items').html(html);
257
+ $('#reserve-confirm').show();
258
+ }
259
+
260
+ const today = new Date().toISOString().slice(0, 10);
261
+ $('#reserve-start').val(today);
262
+ $('#reserve-end').val(today);
263
+ $('#reserve-quantity').val(1);
264
+
265
+ $('#reserve-confirm').off('click').on('click', () => {
266
+ CalendarPage.sendReservation(eid);
267
+ });
268
+
269
+ modal.fadeIn();
270
+ });
271
+ },
272
+
273
+ sendReservation: function (eid) {
274
+ const itemId = $('input[name="reserve-item"]:checked').val();
275
+ const quantity = Number($('#reserve-quantity').val() || 1);
276
+ const dateStart = $('#reserve-start').val();
277
+ const dateEnd = $('#reserve-end').val();
278
+
279
+ if (!itemId) return app.alertError('Sélectionnez un matériel.');
280
+ if (!quantity || quantity <= 0) return app.alertError('Quantité invalide.');
281
+ if (!dateStart || !dateEnd) return app.alertError('Merci de renseigner les dates.');
282
+ if (dateEnd < dateStart) return app.alertError('La date de fin doit être ≥ la date de début.');
283
+
284
+ $.ajax({
285
+ url: '/api/calendar/event/' + eid + '/book',
286
+ method: 'POST',
287
+ contentType: 'application/json',
288
+ data: JSON.stringify({ itemId, quantity, dateStart, dateEnd }),
289
+ })
290
+ .then(res => {
291
+ app.alertSuccess(res.message || 'Demande envoyée.');
292
+ $('#calendar-reserve-modal').fadeOut();
293
+ })
294
+ .catch(err => {
295
+ app.alertError(err.responseJSON?.error || err.message);
296
+ });
297
+ }
298
+ };
299
+
300
+ CalendarPage.init();
301
+ });
@@ -0,0 +1,47 @@
1
+ #calendar {
2
+ margin: 20px;
3
+ }
4
+
5
+ .calendar-modal {
6
+ position: fixed;
7
+ inset: 0;
8
+ z-index: 9000;
9
+ background: rgba(0,0,0,0.5);
10
+ display: none;
11
+ }
12
+
13
+ .calendar-modal-content {
14
+ background: #fff;
15
+ padding: 20px;
16
+ width: 600px;
17
+ margin: 60px auto;
18
+ border-radius: 6px;
19
+ }
20
+
21
+ .calendar-close {
22
+ float: right;
23
+ cursor: pointer;
24
+ }
25
+
26
+ .booking-item-row {
27
+ display: flex;
28
+ gap: 5px;
29
+ margin-bottom: 6px;
30
+ }
31
+
32
+ .booking-item-row input {
33
+ flex: 1;
34
+ }
35
+
36
+ .booking-item-row .item-remove {
37
+ flex: 0 0 auto;
38
+ }
39
+
40
+ .reservation-row {
41
+ background: #eef2f5;
42
+ }
43
+
44
+ .calendar-upcoming-list {
45
+ list-style: none;
46
+ padding-left: 0;
47
+ }
@@ -0,0 +1,28 @@
1
+ <div id="calendar-lite-admin">
2
+ <h2>Calendar Lite – Administration</h2>
3
+
4
+ <h3>Réservations en attente</h3>
5
+ <div id="pending-reservations"></div>
6
+
7
+ <hr>
8
+
9
+ <h3>Paramètres du plugin</h3>
10
+
11
+ <div class="form-group">
12
+ <label>Groupes autorisés à créer / modifier des événements</label>
13
+ <input id="calendar-lite-groups" class="form-control" value="{settings.allowedGroups}">
14
+ <small>Ex : administrators, moderators</small>
15
+ </div>
16
+
17
+ <div class="form-group">
18
+ <label>Nombre d’événements dans le widget</label>
19
+ <input id="calendar-lite-widget-limit" type="number" class="form-control" value="{settings.limit}">
20
+ <label>Groupes autorisés à réserver</label>
21
+ <input id="calendar-lite-book-groups" class="form-control" value="{settings.allowedBookingGroups}">
22
+ <small>Ex : registered-users, membres, vip</small>
23
+ </div>
24
+
25
+ <button id="calendar-lite-save" class="btn btn-primary">Enregistrer</button>
26
+ </div>
27
+
28
+ <script src="/plugins/nodebb-plugin-calendar-lite/static/js/admin.js"></script>
@@ -0,0 +1,30 @@
1
+ <div id="calendar-planning">
2
+ <h2>Planning des réservations de matériel</h2>
3
+
4
+ <p>
5
+ <a href="/admin/plugins/calendar-lite" class="btn btn-default btn-sm">
6
+ ← Retour au plugin Calendar Lite
7
+ </a>
8
+ </p>
9
+
10
+ <table class="table table-striped">
11
+ <thead>
12
+ <tr>
13
+ <th>Événement</th>
14
+ <th>Matériel</th>
15
+ <th>Lieu de retrait</th>
16
+ <th>UID</th>
17
+ <th>Quantité</th>
18
+ <th>Début</th>
19
+ <th>Fin</th>
20
+ <th>Jours</th>
21
+ <th>Statut</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody id="planning-body">
25
+ <tr><td colspan="9">Chargement…</td></tr>
26
+ </tbody>
27
+ </table>
28
+ </div>
29
+
30
+ <script src="/plugins/nodebb-plugin-calendar-lite/static/js/admin.js"></script>
@@ -0,0 +1,45 @@
1
+ <h2>Mes réservations</h2>
2
+
3
+ <table class="table table-striped">
4
+ <thead>
5
+ <tr>
6
+ <th>Événement</th>
7
+ <th>Matériel</th>
8
+ <th>Quantité</th>
9
+ <th>Dates</th>
10
+ <th>Jours</th>
11
+ <th>Statut</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody id="my-reservations-body">
15
+ <tr><td colspan="6">Chargement…</td></tr>
16
+ </tbody>
17
+ </table>
18
+
19
+ <script>
20
+ $(document).ready(function () {
21
+ const tbody = $('#my-reservations-body');
22
+ $.get('/api/calendar/my-reservations').then(rows => {
23
+ tbody.empty();
24
+ if (!rows.length) {
25
+ tbody.append('<tr><td colspan="6">Aucune réservation.</td></tr>');
26
+ return;
27
+ }
28
+ rows.forEach(r => {
29
+ const tr = $(`
30
+ <tr>
31
+ <td>${r.eventTitle}</td>
32
+ <td>${r.itemName}</td>
33
+ <td>${r.quantity}</td>
34
+ <td>${r.dateStart} → ${r.dateEnd}</td>
35
+ <td>${r.days || ''}</td>
36
+ <td>${r.status}</td>
37
+ </tr>
38
+ `);
39
+ tbody.append(tr);
40
+ });
41
+ }).catch(() => {
42
+ tbody.html('<tr><td colspan="6">Erreur lors du chargement.</td></tr>');
43
+ });
44
+ });
45
+ </script>