nodebb-plugin-calendar-onekite 2.1.2 → 10.0.11

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,10 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "2.1.2",
4
- "description": "Calendar + equipment booking + admin approval + HelloAsso payments for NodeBB v4 (no-jQuery UI)",
3
+ "version": "10.0.11",
4
+ "description": "NodeBB calendar booking plugin using FullCalendar and HelloAsso checkout intents",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
- "keywords": [
8
- "nodebb",
9
- "plugin",
10
- "calendar",
11
- "booking",
12
- "helloasso",
13
- "fullcalendar"
14
- ],
15
- "engines": {
16
- "node": ">=18"
17
- },
18
- "dependencies": {},
19
- "scripts": {
20
- "lint": "echo \"(optional) add eslint if you want\"",
21
- "test": "echo \"no tests\""
7
+ "dependencies": {
8
+ "axios": "^1.7.7"
22
9
  }
23
10
  }
package/plugin.json CHANGED
@@ -1,17 +1,9 @@
1
1
  {
2
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.1.2",
3
+ "name": "Calendar OneKite",
4
+ "description": "FullCalendar-based equipment booking workflow with HelloAsso checkout + admin approval.",
5
+ "url": "https://www.onekite.com/calendar",
7
6
  "library": "./library.js",
8
- "staticDirs": {
9
- "static": "static"
10
- },
11
- "acpScripts": [
12
- "static/js/admin.bundle.js",
13
- "static/js/admin-planning.bundle.js"
14
- ],
15
7
  "hooks": [
16
8
  {
17
9
  "hook": "static:app.load",
@@ -22,13 +14,25 @@
22
14
  "method": "addAdminNavigation"
23
15
  },
24
16
  {
25
- "hook": "filter:widgets.getWidgets",
26
- "method": "defineWidgets"
27
- },
28
- {
29
- "hook": "filter:widget.render:calendarUpcoming",
30
- "method": "renderUpcomingWidget"
17
+ "hook": "filter:router.page",
18
+ "method": "addPageRoute"
31
19
  }
32
20
  ],
33
- "templates": "templates"
21
+ "templates": "templates",
22
+ "staticDirs": {
23
+ "public": "./public"
24
+ },
25
+ "acpScripts": [
26
+ "public/js/admin/calendar-onekite-admin.js"
27
+ ],
28
+ "scripts": [
29
+ "public/js/calendar-onekite.js"
30
+ ],
31
+ "styles": [
32
+ "public/css/calendar-onekite.css"
33
+ ],
34
+ "languages": [
35
+ "language"
36
+ ],
37
+ "defaultLang": "en-GB"
34
38
  }
@@ -0,0 +1,14 @@
1
+ .calendar-onekite-page {
2
+ padding: 0.5rem 0;
3
+ }
4
+
5
+ .calendar-onekite-header {
6
+ margin-top: 0.25rem;
7
+ }
8
+
9
+ #onekite-calendar {
10
+ background: var(--bs-body-bg);
11
+ border: 1px solid var(--bs-border-color);
12
+ border-radius: 0.75rem;
13
+ padding: 0.5rem;
14
+ }
@@ -0,0 +1,81 @@
1
+ /* global $, app, socket */
2
+
3
+ (function () {
4
+ function setStatus(html, type) {
5
+ const el = $('#calendar-onekite-admin-status');
6
+ el.html(`<div class="alert alert-${type || 'info'}">${html}</div>`);
7
+ }
8
+
9
+ async function loadSettings() {
10
+ const settings = await $.getJSON('/api/admin/plugins/calendar-onekite');
11
+ // Populate
12
+ $('#calendar-onekite-settings [name="enabled"]').prop('checked', settings.enabled === 'on' || settings.enabled === true);
13
+ $('#calendar-onekite-settings [name="requesterGroups"]').val(settings.requesterGroups || '');
14
+ $('#calendar-onekite-settings [name="approverGroups"]').val(settings.approverGroups || '');
15
+ $('#calendar-onekite-settings [name="holdMinutes"]').val(settings.holdMinutes || 60);
16
+ $('#calendar-onekite-settings [name="notifyEmails"]').val(settings.notifyEmails || '');
17
+
18
+ $('#calendar-onekite-settings [name="helloassoEnv"]').val(settings.helloassoEnv || 'sandbox');
19
+ $('#calendar-onekite-settings [name="helloassoClientId"]').val(settings.helloassoClientId || '');
20
+ $('#calendar-onekite-settings [name="helloassoClientSecret"]').val(settings.helloassoClientSecret || '');
21
+ $('#calendar-onekite-settings [name="helloassoOrganizationSlug"]').val(settings.helloassoOrganizationSlug || '');
22
+ $('#calendar-onekite-settings [name="helloassoFormType"]').val(settings.helloassoFormType || 'event');
23
+ $('#calendar-onekite-settings [name="helloassoFormSlug"]').val(settings.helloassoFormSlug || '');
24
+ $('#calendar-onekite-settings [name="helloassoBackUrl"]').val(settings.helloassoBackUrl || '');
25
+ $('#calendar-onekite-settings [name="helloassoErrorUrl"]').val(settings.helloassoErrorUrl || '');
26
+ $('#calendar-onekite-settings [name="helloassoReturnUrl"]').val(settings.helloassoReturnUrl || '');
27
+ $('#calendar-onekite-settings [name="itemsCacheMinutes"]').val(settings.itemsCacheMinutes || 360);
28
+ }
29
+
30
+ async function saveSettings(e) {
31
+ e.preventDefault();
32
+ const data = {};
33
+ $('#calendar-onekite-settings').serializeArray().forEach(({ name, value }) => {
34
+ data[name] = value;
35
+ });
36
+ data.enabled = $('#calendar-onekite-settings [name="enabled"]').prop('checked') ? 'on' : '';
37
+ await $.ajax({
38
+ url: '/api/admin/plugins/calendar-onekite',
39
+ method: 'POST',
40
+ data,
41
+ });
42
+ setStatus('Sauvegardé ✅', 'success');
43
+ }
44
+
45
+ async function testItems() {
46
+ try {
47
+ const res = await $.getJSON('/api/calendar-onekite/items');
48
+ const items = res.items || [];
49
+ setStatus(`Items récupérés: <strong>${items.length}</strong>`, 'info');
50
+ } catch (e) {
51
+ setStatus(`Erreur items: ${e.responseJSON && e.responseJSON.error ? e.responseJSON.error : e.statusText}`, 'danger');
52
+ }
53
+ }
54
+
55
+ async function purgeYear() {
56
+ const year = parseInt($('#calendar-onekite-purge-year').val(), 10);
57
+ if (!year) {
58
+ return setStatus('Année invalide', 'warning');
59
+ }
60
+ if (!confirm(`Purger toutes les réservations dont le début est en ${year} ?`)) return;
61
+
62
+ try {
63
+ const res = await $.ajax({
64
+ url: '/api/admin/plugins/calendar-onekite/purge',
65
+ method: 'POST',
66
+ data: { year },
67
+ });
68
+ setStatus(`Purge terminée. Supprimés: <strong>${res.deleted}</strong>`, 'success');
69
+ } catch (e) {
70
+ setStatus(`Erreur purge: ${e.responseJSON && e.responseJSON.error ? e.responseJSON.error : e.statusText}`, 'danger');
71
+ }
72
+ }
73
+
74
+ $(window).on('action:admin.plugins.calendar-onekite', async function () {
75
+ await loadSettings();
76
+
77
+ $('#calendar-onekite-settings').off('submit').on('submit', saveSettings);
78
+ $('#calendar-onekite-test-items').off('click').on('click', testItems);
79
+ $('#calendar-onekite-purge').off('click').on('click', purgeYear);
80
+ });
81
+ })();
@@ -0,0 +1,186 @@
1
+ /* global $, app */
2
+ (function () {
3
+ async function fetchItems() {
4
+ const res = await $.getJSON('/api/calendar-onekite/items');
5
+ return res.items || [];
6
+ }
7
+
8
+ async function createReservation(payload) {
9
+ const res = await $.ajax({
10
+ url: '/api/calendar-onekite/reservations',
11
+ method: 'POST',
12
+ data: payload,
13
+ });
14
+ return res.reservation;
15
+ }
16
+
17
+ async function approveReservation(id) {
18
+ const res = await $.ajax({
19
+ url: `/api/calendar-onekite/reservations/${encodeURIComponent(id)}/approve`,
20
+ method: 'POST',
21
+ });
22
+ return res.reservation;
23
+ }
24
+
25
+ async function rejectReservation(id) {
26
+ const res = await $.ajax({
27
+ url: `/api/calendar-onekite/reservations/${encodeURIComponent(id)}/reject`,
28
+ method: 'POST',
29
+ });
30
+ return res.reservation;
31
+ }
32
+
33
+ function loadFullCalendar() {
34
+ return new Promise((resolve, reject) => {
35
+ if (window.FullCalendar) return resolve();
36
+ const css = document.createElement('link');
37
+ css.rel = 'stylesheet';
38
+ css.href = 'https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.css';
39
+ document.head.appendChild(css);
40
+
41
+ const script = document.createElement('script');
42
+ script.src = 'https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js';
43
+ script.onload = resolve;
44
+ script.onerror = reject;
45
+ document.head.appendChild(script);
46
+ });
47
+ }
48
+
49
+ async function initCalendar() {
50
+ await loadFullCalendar();
51
+
52
+ const uid = parseInt(app.user && app.user.uid, 10) || 0;
53
+ if (!uid) $('#onekite-login-hint').show();
54
+
55
+ let items = [];
56
+ try {
57
+ items = await fetchItems();
58
+ } catch (e) {
59
+ console.warn('Unable to fetch items', e);
60
+ }
61
+
62
+ function promptForItem() {
63
+ if (!items.length) {
64
+ const itemName = prompt('Nom du matériel ?');
65
+ if (!itemName) return null;
66
+ return { id: itemName, name: itemName, priceCents: 0 };
67
+ }
68
+ const choices = items
69
+ .map((it, idx) => {
70
+ const id = it.id ?? it.itemId ?? it.itemID ?? idx;
71
+ const name = it.name ?? it.label ?? it.itemName ?? `Item ${id}`;
72
+ const price = it.price ?? it.amount ?? it.unitPrice ?? it.totalAmount ?? it.publicAmount ?? 0;
73
+ return { id: String(id), name: String(name), priceCents: Number(price) || 0 };
74
+ });
75
+
76
+ const menu = choices.map((c, i) => `${i + 1}) ${c.name}${c.priceCents ? ` — ${(c.priceCents / 100).toFixed(2)}€` : ''}`).join('\n');
77
+ const input = prompt(`Choisis un matériel:\n${menu}\n\nEntre le numéro:`);
78
+ const n = parseInt(input, 10);
79
+ if (!n || n < 1 || n > choices.length) return null;
80
+ return choices[n - 1];
81
+ }
82
+
83
+ const calendarEl = document.getElementById('onekite-calendar');
84
+ const calendar = new FullCalendar.Calendar(calendarEl, {
85
+ initialView: 'dayGridMonth',
86
+ selectable: true,
87
+ editable: false,
88
+ height: 'auto',
89
+ headerToolbar: {
90
+ left: 'prev,next today',
91
+ center: 'title',
92
+ right: 'dayGridMonth,timeGridWeek,timeGridDay',
93
+ },
94
+ events: async (info, success, failure) => {
95
+ try {
96
+ const res = await $.getJSON('/api/calendar-onekite/events', {
97
+ start: info.startStr,
98
+ end: info.endStr,
99
+ });
100
+ success(res.events || []);
101
+ } catch (e) {
102
+ failure(e);
103
+ }
104
+ },
105
+ select: async (selectionInfo) => {
106
+ try {
107
+ if (!uid) {
108
+ app.alertError('Connecte-toi pour réserver.');
109
+ calendar.unselect();
110
+ return;
111
+ }
112
+ const item = promptForItem();
113
+ if (!item) {
114
+ calendar.unselect();
115
+ return;
116
+ }
117
+ const payload = {
118
+ itemId: item.id,
119
+ itemName: item.name,
120
+ start: selectionInfo.startStr,
121
+ end: selectionInfo.endStr,
122
+ priceCents: item.priceCents || 0,
123
+ };
124
+ await createReservation(payload);
125
+ app.alertSuccess('Demande envoyée (en attente de validation).');
126
+ calendar.refetchEvents();
127
+ } catch (e) {
128
+ const err = (e.responseJSON && e.responseJSON.error) ? e.responseJSON.error : e.statusText;
129
+ if (err === 'overlap') {
130
+ app.alertError('Ce matériel est déjà réservé sur cette période.');
131
+ } else if (err === 'not-allowed') {
132
+ app.alertError('Vous n’êtes pas autorisé à créer une demande.');
133
+ } else {
134
+ app.alertError(`Erreur: ${err}`);
135
+ }
136
+ } finally {
137
+ calendar.unselect();
138
+ }
139
+ },
140
+ eventClick: async (clickInfo) => {
141
+ const ev = clickInfo.event;
142
+ const p = ev.extendedProps || {};
143
+ // Approve/reject only if pending, and user has rights (server will enforce)
144
+ if (p.status !== 'pending') return;
145
+
146
+ if (!uid) return;
147
+
148
+ const action = prompt('Réservation en attente. Tape "a" pour valider, "r" pour refuser, autre pour annuler.');
149
+ if (action === 'a') {
150
+ try {
151
+ const updated = await approveReservation(ev.id);
152
+ app.alertSuccess('Validée ✅. Un email avec lien de paiement a été envoyé.');
153
+ calendar.refetchEvents();
154
+ if (updated && updated.paymentUrl) {
155
+ // optionally open link
156
+ const open = confirm('Ouvrir le lien de paiement ?');
157
+ if (open) window.open(updated.paymentUrl, '_blank');
158
+ }
159
+ } catch (e) {
160
+ const err = (e.responseJSON && e.responseJSON.error) ? e.responseJSON.error : e.statusText;
161
+ app.alertError(`Erreur validation: ${err}`);
162
+ }
163
+ } else if (action === 'r') {
164
+ try {
165
+ await rejectReservation(ev.id);
166
+ app.alertSuccess('Refusée ❌');
167
+ calendar.refetchEvents();
168
+ } catch (e) {
169
+ const err = (e.responseJSON && e.responseJSON.error) ? e.responseJSON.error : e.statusText;
170
+ app.alertError(`Erreur refus: ${err}`);
171
+ }
172
+ }
173
+ },
174
+ });
175
+
176
+ calendar.render();
177
+
178
+ $('#onekite-refresh').on('click', () => calendar.refetchEvents());
179
+ }
180
+
181
+ $(window).on('action:ajaxify.end', function (ev, data) {
182
+ if (data && data.url && data.url.startsWith('calendar')) {
183
+ initCalendar();
184
+ }
185
+ });
186
+ })();
@@ -1,78 +1,117 @@
1
- <div id="calendar-onekite-admin">
2
- <h2>Calendar Onekite – Administration</h2>
1
+ <div class="acp-page-container">
2
+ <h1 class="mb-4">Calendar OneKite</h1>
3
3
 
4
- <h3>Réservations en attente</h3>
5
- <div id="pending-reservations"></div>
4
+ <div class="alert alert-warning">
5
+ Les groupes sont à renseigner par <strong>noms de groupes</strong> séparés par des virgules (ex: <code>administrators, booking-approvers</code>).
6
+ </div>
6
7
 
7
- <hr>
8
+ <form id="calendar-onekite-settings">
9
+ <div class="mb-3">
10
+ <label class="form-label">Activer le plugin</label>
11
+ <div class="form-check">
12
+ <input class="form-check-input" type="checkbox" id="enabled" name="enabled">
13
+ <label class="form-check-label" for="enabled">Enabled</label>
14
+ </div>
15
+ </div>
8
16
 
9
- <h3>Paramètres du plugin</h3>
17
+ <div class="row g-3">
18
+ <div class="col-md-6">
19
+ <label class="form-label">Groupes autorisés à créer une demande (requesterGroups)</label>
20
+ <input class="form-control" type="text" name="requesterGroups" placeholder="members, ..." />
21
+ </div>
22
+ <div class="col-md-6">
23
+ <label class="form-label">Groupes qui valident/refusent (approverGroups)</label>
24
+ <input class="form-control" type="text" name="approverGroups" placeholder="administrators, ..." />
25
+ </div>
26
+ </div>
10
27
 
11
- <div class="form-group">
12
- <label>Groupes autorisés à créer / modifier des événements</label>
13
- <input id="calendar-onekite-groups" class="form-control" value="{settings.allowedGroups}">
14
- <small>Ex : administrators, moderators</small>
15
- </div>
28
+ <div class="row g-3 mt-1">
29
+ <div class="col-md-6">
30
+ <label class="form-label">Durée de blocage d'une demande en attente (minutes)</label>
31
+ <input class="form-control" type="number" name="holdMinutes" min="1" value="60" />
32
+ </div>
33
+ <div class="col-md-6">
34
+ <label class="form-label">Emails supplémentaires à notifier (optionnel)</label>
35
+ <input class="form-control" type="text" name="notifyEmails" placeholder="[email protected], [email protected]" />
36
+ </div>
37
+ </div>
16
38
 
17
- <div class="form-group">
18
- <label>Groupes autorisés à réserver</label>
19
- <input id="calendar-onekite-book-groups" class="form-control" value="{settings.allowedBookingGroups}">
20
- <small>Ex : registered-users, membres, vip</small>
21
- </div>
39
+ <hr class="my-4"/>
22
40
 
23
- <div class="form-group">
24
- <label>Nombre d’événements dans le widget</label>
25
- <input id="calendar-onekite-widget-limit" type="number" class="form-control" value="{settings.limit}">
26
- </div>
41
+ <h3 class="h5">HelloAsso</h3>
42
+ <div class="row g-3">
43
+ <div class="col-md-4">
44
+ <label class="form-label">Environnement</label>
45
+ <select class="form-select" name="helloassoEnv">
46
+ <option value="sandbox">sandbox</option>
47
+ <option value="prod">prod</option>
48
+ </select>
49
+ </div>
50
+ <div class="col-md-4">
51
+ <label class="form-label">Client ID</label>
52
+ <input class="form-control" type="text" name="helloassoClientId" />
53
+ </div>
54
+ <div class="col-md-4">
55
+ <label class="form-label">Client Secret</label>
56
+ <input class="form-control" type="password" name="helloassoClientSecret" />
57
+ </div>
58
+ </div>
27
59
 
28
- <hr>
29
- <h3>Lieux & Inventaire (global)</h3>
30
- <p class="text-muted">
31
- Les stocks sont globaux et séparés par lieu. Format JSON.
32
- </p>
60
+ <div class="row g-3 mt-1">
61
+ <div class="col-md-4">
62
+ <label class="form-label">organizationSlug</label>
63
+ <input class="form-control" type="text" name="helloassoOrganizationSlug" />
64
+ </div>
65
+ <div class="col-md-4">
66
+ <label class="form-label">formType</label>
67
+ <input class="form-control" type="text" name="helloassoFormType" placeholder="event, membership, donation..." />
68
+ </div>
69
+ <div class="col-md-4">
70
+ <label class="form-label">formSlug</label>
71
+ <input class="form-control" type="text" name="helloassoFormSlug" />
72
+ </div>
73
+ </div>
33
74
 
34
- <div class="form-group">
35
- <label>Lieux (locations)</label>
36
- <textarea id="calendar-onekite-locations" class="form-control" rows="4">{settings.locationsJson}</textarea>
37
- <small>Ex: [{"id":"arnaud","name":"Chez Arnaud"},{"id":"siege","name":"Siège Onekite"}]</small>
38
- </div>
75
+ <div class="row g-3 mt-1">
76
+ <div class="col-md-4">
77
+ <label class="form-label">backUrl (HTTPS)</label>
78
+ <input class="form-control" type="text" name="helloassoBackUrl" />
79
+ </div>
80
+ <div class="col-md-4">
81
+ <label class="form-label">errorUrl (HTTPS)</label>
82
+ <input class="form-control" type="text" name="helloassoErrorUrl" />
83
+ </div>
84
+ <div class="col-md-4">
85
+ <label class="form-label">returnUrl (HTTPS)</label>
86
+ <input class="form-control" type="text" name="helloassoReturnUrl" />
87
+ </div>
88
+ </div>
39
89
 
40
- <div class="form-group">
41
- <label>Inventaire (inventory)</label>
42
- <textarea id="calendar-onekite-inventory" class="form-control" rows="6">{settings.inventoryJson}</textarea>
43
- <small>Ex: [{"id":"wing","name":"Aile Wing","price":5,"stockByLocation":{"arnaud":1,"siege":0}}]</small>
44
- </div>
90
+ <div class="row g-3 mt-1">
91
+ <div class="col-md-4">
92
+ <label class="form-label">Cache items HelloAsso (minutes)</label>
93
+ <input class="form-control" type="number" name="itemsCacheMinutes" min="1" value="360" />
94
+ </div>
95
+ </div>
45
96
 
46
- <hr>
47
- <h3>HelloAsso</h3>
48
- <div class="form-group">
49
- <label>API base</label>
50
- <input id="calendar-onekite-helloasso-apibase" class="form-control" value="{settings.helloassoApiBase}">
51
- <small>Ex: https://api.helloasso.com</small>
52
- </div>
53
- <div class="form-group">
54
- <label>Organization slug</label>
55
- <input id="calendar-onekite-helloasso-org" class="form-control" value="{settings.helloassoOrganizationSlug}">
56
- </div>
57
- <div class="form-group">
58
- <label>Form slug (Checkout)</label>
59
- <input id="calendar-onekite-helloasso-form" class="form-control" value="{settings.helloassoFormSlug}">
60
- </div>
61
- <div class="form-group">
62
- <label>Client ID</label>
63
- <input id="calendar-onekite-helloasso-clientid" class="form-control" value="{settings.helloassoClientId}">
64
- </div>
65
- <div class="form-group">
66
- <label>Client Secret</label>
67
- <input id="calendar-onekite-helloasso-secret" class="form-control" value="{settings.helloassoClientSecret}">
68
- </div>
69
- <div class="form-group">
70
- <label>Return URL</label>
71
- <input id="calendar-onekite-helloasso-return" class="form-control" value="{settings.helloassoReturnUrl}">
72
- </div>
97
+ <div class="mt-4 d-flex gap-2">
98
+ <button class="btn btn-primary" type="submit">Sauvegarder</button>
99
+ <button class="btn btn-outline-secondary" type="button" id="calendar-onekite-test-items">Tester la récupération des items</button>
100
+ </div>
101
+ </form>
73
102
 
74
- <button id="calendar-onekite-save" class="btn btn-primary">Enregistrer</button>
75
- </div>
103
+ <hr class="my-4"/>
76
104
 
105
+ <h3 class="h5">Purge calendrier</h3>
106
+ <div class="row g-2 align-items-end">
107
+ <div class="col-md-3">
108
+ <label class="form-label">Année</label>
109
+ <input class="form-control" type="number" id="calendar-onekite-purge-year" min="1970" max="3000" placeholder="2025" />
110
+ </div>
111
+ <div class="col-md-3">
112
+ <button class="btn btn-danger" type="button" id="calendar-onekite-purge">Purger</button>
113
+ </div>
114
+ </div>
77
115
 
78
- <script src="/plugins/nodebb-plugin-calendar-onekite/static/js/admin.bundle.js"></script>
116
+ <div class="mt-3" id="calendar-onekite-admin-status"></div>
117
+ </div>
@@ -0,0 +1,18 @@
1
+ <div class="calendar-onekite-page">
2
+ <div class="calendar-onekite-header d-flex align-items-center justify-content-between flex-wrap gap-2">
3
+ <h1 class="h3 mb-0">Calendrier</h1>
4
+ <div class="calendar-onekite-actions d-flex gap-2 align-items-center">
5
+ <button class="btn btn-sm btn-outline-secondary" id="onekite-refresh">Rafraîchir</button>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="alert alert-info mt-3" id="onekite-login-hint" style="display:none;">
10
+ Vous devez être connecté pour faire une demande de réservation.
11
+ </div>
12
+
13
+ <div class="mt-3" id="onekite-calendar"></div>
14
+
15
+ <div class="mt-3 small text-muted">
16
+ <span>⏳ = en attente de validation</span> · <span>✅ = validée</span> · <span>❌ = refusée</span>
17
+ </div>
18
+ </div>
package/.drone.yml DELETED
@@ -1,62 +0,0 @@
1
- kind: pipeline
2
- type: docker
3
- name: deploy-nodebb-plugin
4
-
5
- trigger:
6
- branch:
7
- - master
8
- event:
9
- - push
10
-
11
- steps:
12
- - name: sanity-check
13
- image: node:20-alpine
14
- commands:
15
- - node -v
16
- - test -f plugin.json
17
- - test -f library.js
18
- - test -d static
19
- - test -d templates
20
-
21
- - name: deploy-files
22
- image: appleboy/drone-scp
23
- settings:
24
- host:
25
- from_secret: SSH_HOST
26
- username:
27
- from_secret: SSH_USER
28
- port:
29
- from_secret: SSH_PORT
30
- key:
31
- from_secret: SSH_KEY
32
- source:
33
- - ./
34
- target:
35
- from_secret: PLUGIN_TARGET_DIR
36
- strip_components: 0
37
- overwrite: true
38
-
39
- - name: install-and-build
40
- image: appleboy/drone-ssh
41
- environment:
42
- PLUGIN_TARGET_DIR:
43
- from_secret: PLUGIN_TARGET_DIR
44
- settings:
45
- host:
46
- from_secret: SSH_HOST
47
- username:
48
- from_secret: SSH_USER
49
- port:
50
- from_secret: SSH_PORT
51
- key:
52
- from_secret: SSH_KEY
53
- script:
54
- - 'echo "Plugin: $PLUGIN_TARGET_DIR"'
55
- - 'test -d "$PLUGIN_TARGET_DIR"'
56
-
57
- - 'cd "$PLUGIN_TARGET_DIR"'
58
- - 'if [ -f package.json ]; then npm ci --omit=dev; else echo "No package.json, skipping npm ci"; fi'
59
-
60
- #- 'cd "$NODEBB_PATH"'
61
- #- './nodebb build'
62
- #- './nodebb restart || true'
package/README.md DELETED
@@ -1,28 +0,0 @@
1
- # nodebb-plugin-calendar-onekite (no-jQuery)
2
-
3
- ## Features
4
- - FullCalendar frontend (Month/Week/Day/List) with drag&drop + resize (editable for authorized groups)
5
- - Event CRUD, booking (multi-day), admin validation workflow, HelloAsso payment intent + webhook
6
- - Admin pages (ACP) without jQuery; robust under ajaxify via MutationObserver
7
- - Widget: calendarUpcoming
8
-
9
- ## Important: FullCalendar assets
10
- This package loads FullCalendar from jsDelivr CDN in templates/calendar.tpl.
11
- If you need offline / no-CDN, tell me and I will vendor the files under static/vendor/fullcalendar.
12
-
13
- ## Install
14
- - Copy into your NodeBB plugins folder
15
- - Activate plugin in ACP
16
- - Rebuild: ./nodebb build
17
-
18
- ## Routes
19
- - Pages: /calendar, /calendar/my-reservations
20
- - Admin: /admin/plugins/calendar-onekite, /admin/calendar/planning
21
- - API: available under /api/... and /api/v3/... (NodeBB v4 client helper uses /api/v3)
22
-
23
-
24
- ## v2.1 inventory model
25
- - Global locations/inventory in ACP (locationsJson/inventoryJson)
26
- - Events select allowed inventory item IDs (bookingItemIds)
27
- - Availability is global per (itemId, locationId) across all events
28
- - Admin planning is graphical (FullCalendar) with filters