nodebb-plugin-equipment-calendar 0.9.9 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "0.9.9",
3
+ "version": "1.0.1",
4
4
  "description": "Equipment reservation calendar for NodeBB (FullCalendar, approvals, HelloAsso payments)",
5
5
  "main": "library.js",
6
6
  "scripts": {
package/plugin.json CHANGED
@@ -25,6 +25,6 @@
25
25
  "scripts": [
26
26
  "public/js/client.js"
27
27
  ],
28
- "version": "0.5.9",
28
+ "version": "0.6.2",
29
29
  "minver": "4.7.1"
30
30
  }
@@ -2,6 +2,59 @@
2
2
  /* global window, document, FullCalendar, bootbox */
3
3
 
4
4
  (function () {
5
+ function populateItemsSelect() {
6
+ const sel = document.getElementById('ec-item-ids');
7
+ if (!sel) return;
8
+ const items = Array.isArray(window.EC_ITEMS) ? window.EC_ITEMS : [];
9
+ sel.innerHTML = '';
10
+ items.forEach((it) => {
11
+ if (!it || it.active === false) return;
12
+ const opt = document.createElement('option');
13
+ opt.value = String(it.id);
14
+ const p = parseFloat(it.price || '0') || 0;
15
+ const label = (Number.isInteger(p) ? String(p) : p.toFixed(2)) + ' €';
16
+ opt.textContent = `${it.name} — ${label}`;
17
+ opt.setAttribute('data-price', String(p));
18
+ sel.appendChild(opt);
19
+ });
20
+ }
21
+
22
+
23
+ function toUtcMidnightMs(dateObj) {
24
+ return Date.UTC(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
25
+ }
26
+
27
+ function formatDateInputValue(dateObj) {
28
+ // YYYY-MM-DD in UTC
29
+ const y = dateObj.getUTCFullYear();
30
+ const m = String(dateObj.getUTCMonth() + 1).padStart(2, '0');
31
+ const d = String(dateObj.getUTCDate()).padStart(2, '0');
32
+ return `${y}-${m}-${d}`;
33
+ }
34
+
35
+ function openCreateModal(startMs, endMs) {
36
+ const startDateEl = document.getElementById('ec-start-date');
37
+ const endDateEl = document.getElementById('ec-end-date');
38
+ const startMsEl = document.getElementById('ec-start-ms');
39
+ const endMsEl = document.getElementById('ec-end-ms');
40
+
41
+ if (startMsEl) startMsEl.value = String(startMs);
42
+ if (endMsEl) endMsEl.value = String(endMs);
43
+
44
+ // set date inputs
45
+ if (startDateEl) startDateEl.value = formatDateInputValue(new Date(startMs));
46
+ if (endDateEl) endDateEl.value = formatDateInputValue(new Date(endMs - 24*60*60*1000));
47
+
48
+ if (typeof updateTotalPrice === 'function') updateTotalPrice();
49
+
50
+ const modalEl = document.getElementById('ec-create-modal');
51
+ if (modalEl && window.bootstrap && window.bootstrap.Modal) {
52
+ const modal = window.bootstrap.Modal.getOrCreateInstance(modalEl);
53
+ modal.show();
54
+ }
55
+ }
56
+
57
+
5
58
  function overlaps(aStartMs, aEndMs, bStartMs, bEndMs) {
6
59
  return aStartMs < bEndMs && aEndMs > bStartMs;
7
60
  }
@@ -20,8 +73,8 @@
20
73
 
21
74
  const blockedByItem = {};
22
75
  for (const b of blocks) {
23
- if (!blockedByItem[b.itemId]) blockedByItem[b.itemId] = [];
24
- blockedByItem[b.itemId].push(b);
76
+ if (!blockedByItem[b.itemIds]) blockedByItem[b.itemIds] = [];
77
+ blockedByItem[b.itemIds].push(b);
25
78
  }
26
79
 
27
80
  const available = [];
@@ -33,7 +86,7 @@
33
86
  return available;
34
87
  }
35
88
 
36
- function submitReservation(startISO, endISO, itemId, notes) {
89
+ function submitReservation(startISO, endISO, itemIds, notes) {
37
90
  const form = document.getElementById('ec-create-form');
38
91
  if (!form) return;
39
92
 
@@ -44,7 +97,7 @@
44
97
 
45
98
  if (startInput) startInput.value = startISO;
46
99
  if (endInput) endInput.value = endISO;
47
- if (itemInput) itemInput.value = itemId;
100
+ if (itemInput) itemInput.value = itemIds;
48
101
  if (notesInput) notesInput.value = notes || '';
49
102
 
50
103
  form.submit();
@@ -97,8 +150,8 @@
97
150
 
98
151
  if (typeof bootbox === 'undefined') {
99
152
  // Fallback without bootbox (should be rare on NodeBB pages)
100
- const itemId = available[0].id;
101
- submitReservation(startDate.toISOString(), endDate.toISOString(), itemId, '');
153
+ const itemIds = available[0].id;
154
+ submitReservation(startDate.toISOString(), endDate.toISOString(), itemIds, '');
102
155
  return;
103
156
  }
104
157
 
@@ -135,36 +188,38 @@
135
188
 
136
189
  const events = window.EC_EVENTS || [];
137
190
  const initialDate = window.EC_INITIAL_DATE;
138
- const initialView = window.EC_INITIAL_VIEW || 'dayGridMonth';
191
+ const initialView = 'dayGridMonth';
139
192
 
140
193
  const calendar = new FullCalendar.Calendar(el, {
141
194
  initialView: initialView,
142
195
  initialDate: initialDate,
143
196
  timeZone: window.EC_TZ || 'local',
144
197
  selectable: window.EC_CAN_CREATE === true,
198
+ allDaySlot: true,
199
+ slotDuration: '24:00',
200
+ eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
145
201
  selectMirror: true,
146
202
  events: events,
203
+ displayEventTime: false,
147
204
 
148
205
  // Range selection (drag)
149
- select: function (info) {
150
- openNodeBBModal(info.startStr, info.endStr);
206
+ select: function(info) {
207
+ const startMs = toUtcMidnightMs(info.start);
208
+ const endMs = toUtcMidnightMs(info.end);
209
+ openCreateModal(startMs, endMs);
151
210
  },
152
211
 
153
212
  // Single date click
154
- dateClick: function (info) {
155
- // Default: 1h slot
156
- const start = info.date;
157
- const end = new Date(start.getTime() + 60 * 60 * 1000);
158
- openNodeBBModal(start.toISOString(), end.toISOString());
213
+ dateClick: function(info) {
214
+ const startMs = toUtcMidnightMs(info.date);
215
+ const endMs = startMs + 24*60*60*1000;
216
+ openCreateModal(startMs, endMs);
159
217
  },
160
218
 
161
- headerToolbar: {
162
- left: 'prev,next today',
163
- center: 'title',
164
- right: 'dayGridMonth,timeGridWeek,timeGridDay'
165
- },
219
+ headerToolbar: { left: 'prev,next today', center: 'title', right: '' },
166
220
  });
167
221
 
222
+ populateItemsSelect();
168
223
  calendar.render();
169
224
  }
170
225
 
@@ -1,135 +1,112 @@
1
1
  <div class="acp-page-container">
2
2
  <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
3
- <h1 class="mb-0">Equipment Calendar</h1>
4
- <div class="btn-group">
5
- <a class="btn btn-secondary active" href="/admin/plugins/equipment-calendar">Paramètres</a>
6
- <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations">Réservations</a>
7
- <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/helloasso-test">Test HelloAsso</a>
3
+ <h1 class="mb-0">Equipment Calendar</h1>
4
+ <div class="btn-group">
5
+ <a class="btn btn-secondary active" href="/admin/plugins/equipment-calendar">Paramètres</a>
6
+ <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations">Réservations</a>
7
+ <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/helloasso-test">Test HelloAsso</a>
8
+ </div>
8
9
  </div>
9
- </div>
10
-
11
- {{{ if saved }}}
12
- <div class="alert alert-success">Paramètres enregistrés.</div>
13
- <script>
14
- (function () {
15
- try {
16
- if (window.app && window.app.alertSuccess) {
17
- window.app.alertSuccess('Paramètres enregistrés');
18
- }
19
- } catch (e) {}
20
- }());
21
- </script>
22
- {{{ end }}}
23
10
 
24
-
25
- <form id="ec-admin-form" method="post" action="/admin/plugins/equipment-calendar/save" class="mb-3">
11
+ <form id="ec-admin-form" method="post" action="/admin/plugins/equipment-calendar/save" class="mb-3 mt-3">
26
12
  <input type="hidden" name="_csrf" value="{config.csrf_token}">
27
13
 
28
- <div class="alert alert-warning">
29
- Le champ "Matériel" doit être un JSON valide (array). Exemple :
30
- <pre class="mb-0">[
31
- { "id": "cam1", "name": "Caméra A", "price": 50, "": "Stock A", "active": true },
32
- { "id": "light1", "name": "Projecteur", "price": 20, "": "Stock B", "active": true }
33
- ]</pre>
34
- <div class="mt-2">Note : <strong>price est en euros</strong> dans ce plugin (ex: 50 = 50€).</div>
35
- </div>
36
-
37
14
  <div class="card card-body mb-3">
38
15
  <h5>Permissions</h5>
16
+
17
+ <div class="mb-3">
18
+ <label class="form-label">Groupes autorisés à créer une demande (creatorGroups)</label>
19
+ <input class="form-control" type="text" name="creatorGroups" value="{settings.creatorGroups}">
20
+ <div class="form-text">Nom(s) de groupes NodeBB. Pour plusieurs groupes, sépare par des virgules. Ex: <code>registered-users, members</code></div>
39
21
  </div>
40
22
 
41
23
  <div class="mb-3">
42
- <label class="form-label">Matériel manuel (JSON) — utilisé uniquement si Source=Manuel</label>
43
- <textarea name="itemsJson" class="form-control" rows="10">{settings.itemsJson}</textarea>
24
+ <label class="form-label">Groupe valideur (approverGroup)</label>
25
+ <input class="form-control" type="text" name="approverGroup" value="{settings.approverGroup}">
26
+ <div class="form-text">Les membres de ce groupe voient /equipment/approvals et peuvent valider/refuser.</div>
44
27
  </div>
45
- </div>
46
28
 
47
- <div class="card card-body mb-3">
48
- <h5>HelloAsso</h5>
49
- <div class="mb-3"><label class="form-label">Client ID</label><input name="ha_clientId" class="form-control" value="{settings.ha_clientId}"></div>
50
- <div class="mb-3"><label class="form-label">Client Secret</label><input name="ha_clientSecret" class="form-control" value="{settings.ha_clientSecret}"></div>
51
- <div class="mb-3"><label class="form-label">Organization Slug</label><input name="ha_organizationSlug" class="form-control" value="{settings.ha_organizationSlug}"></div>
52
- <div class="mb-3"><label class="form-label">Return URL</label><input name="ha_returnUrl" class="form-control" value="{settings.ha_returnUrl}"></div>
53
- <div class="mb-3"><label class="form-label">Webhook Secret (HMAC SHA256)</label><input name="ha_webhookSecret" class="form-control" value="{settings.ha_webhookSecret}"></div>
29
+ <div class="mb-0">
30
+ <label class="form-label">Groupe notifié (notifyGroup)</label>
31
+ <input class="form-control" type="text" name="notifyGroup" value="{settings.notifyGroup}">
32
+ <div class="form-text">Envoi d’un email/notification au groupe lors d’une nouvelle demande.</div>
33
+ </div>
54
34
  </div>
55
35
 
56
36
  <div class="card card-body mb-3">
57
- <h5>Calendrier</h5>
37
+ <h5>Matériel</h5>
38
+
39
+ <div class="mb-3">
40
+ <label class="form-label">Source du matériel</label>
41
+ <select class="form-select" name="itemsSource">
42
+ <option value="manual" {{{ if view_itemsSourceManual }}}selected{{{ end }}}>Manuel (JSON)</option>
43
+ <option value="helloasso" {{{ if view_itemsSourceHelloasso }}}selected{{{ end }}}>HelloAsso (catalogue)</option>
44
+ </select>
45
+ </div>
46
+
58
47
  <div class="mb-3">
48
+ <label class="form-label">Matériel (JSON) — utilisé si Source = Manuel</label>
49
+ <textarea class="form-control" name="itemsJson" rows="8">{settings.itemsJson}</textarea>
50
+ <div class="form-text">Format: <code>[{"id":"cam1","name":"Caméra","price":50,"active":true}]</code> — <code>price</code> en euros (unitaire / jour).</div>
51
+ </div>
52
+
53
+ <div class="mb-0">
59
54
  <label class="form-label">Timeout paiement (minutes)</label>
60
55
  <input class="form-control" type="number" min="1" name="paymentTimeoutMinutes" value="{settings.paymentTimeoutMinutes}">
61
56
  <div class="form-text">Après validation, si le paiement n’est pas confirmé dans ce délai, la réservation est annulée et le matériel est débloqué.</div>
62
57
  </div>
63
- <div class="mb-3">
64
- <label class="form-label">Vue par défaut</label>
65
- <select name="defaultView" class="form-select">
66
- <option value="dayGridMonth" {{{ if view_dayGridMonth }}}selected{{{ end }}}>Mois</option>
67
- <option value="timeGridWeek" {{{ if view_timeGridWeek }}}selected{{{ end }}}>Semaine</option>
68
- <option value="timeGridDay" {{{ if view_timeGridDay }}}selected{{{ end }}}>Jour</option>
69
- </select>
70
- </div>
71
- <div class="mb-3"><label class="form-label">Timezone</label><input name="timezone" class="form-control" value="{settings.timezone}"></div>
72
58
  </div>
73
59
 
74
-
75
60
  <div class="card card-body mb-3">
76
- <h5>Purge</h5>
77
- <p class="text-muted mb-2">Supprime des réservations de la base (action irréversible).</p>
78
-
79
- <div class="d-flex flex-wrap gap-2">
80
- <button type="submit" class="btn btn-outline-danger" formaction="/admin/plugins/equipment-calendar/purge" formmethod="post" name="mode" value="nonblocking"
81
- onclick="return confirm('Supprimer toutes les réservations refusées/annulées ?');">
82
- Purger refusées/annulées
83
- </button>
84
-
85
- <div class="d-flex align-items-center gap-2">
86
- <input class="form-control" style="max-width: 140px" type="number" min="1" name="olderThanDays" placeholder="Jours">
87
- <button type="submit" class="btn btn-outline-danger" formaction="/admin/plugins/equipment-calendar/purge" formmethod="post" name="mode" value="olderThan"
88
- onclick="return confirm('Supprimer les réservations plus anciennes que N jours ?');">
89
- Purger plus anciennes que…
90
- </button>
91
- </div>
61
+ <h5>HelloAsso</h5>
92
62
 
93
- <button type="submit" class="btn btn-danger" formaction="/admin/plugins/equipment-calendar/purge" formmethod="post" name="mode" value="all"
94
- onclick="return confirm('⚠️ Supprimer TOUTES les réservations ?');">
95
- Tout purger
96
- </button>
63
+ <div class="mb-3">
64
+ <label class="form-label">API Base URL (prod/sandbox)</label>
65
+ <input class="form-control" type="text" name="ha_apiBaseUrl" value="{settings.ha_apiBaseUrl}">
66
+ <div class="form-text">Production: <code>https://api.helloasso.com</code> — Sandbox: <code>https://api.helloasso-sandbox.com</code></div>
97
67
  </div>
98
68
 
99
- {{{ if purged }}}
100
- <div class="alert alert-success mt-3">Purge effectuée : {purged} réservation(s) supprimée(s).</div>
101
- <script>
102
- (function () {
103
- try {
104
- if (window.app && window.app.alertSuccess) {
105
- window.app.alertSuccess('Purge effectuée : {purged} réservation(s) supprimée(s).');
106
- }
107
- } catch (e) {}
108
- }());
109
- </script>
110
- {{{ end }}}
111
- </div>
69
+ <div class="mb-3">
70
+ <label class="form-label">Organization slug</label>
71
+ <input class="form-control" type="text" name="ha_organizationSlug" value="{settings.ha_organizationSlug}">
72
+ </div>
112
73
 
113
- <button id="ec-save" class="btn btn-primary" type="button">Sauvegarder</button>
74
+ <div class="mb-3">
75
+ <label class="form-label">Client ID</label>
76
+ <input class="form-control" type="text" name="ha_clientId" value="{settings.ha_clientId}">
77
+ </div>
78
+
79
+ <div class="mb-3">
80
+ <label class="form-label">Client Secret</label>
81
+ <input class="form-control" type="password" name="ha_clientSecret" value="{settings.ha_clientSecret}">
82
+ </div>
114
83
 
84
+ <div class="row g-3">
85
+ <div class="col-md-6">
86
+ <label class="form-label">Form Type</label>
87
+ <input class="form-control" type="text" name="ha_itemsFormType" value="{settings.ha_itemsFormType}" placeholder="shop">
88
+ <div class="form-text">Ex: <code>shop</code></div>
89
+ </div>
90
+ <div class="col-md-6">
91
+ <label class="form-label">Form Slug</label>
92
+ <input class="form-control" type="text" name="ha_itemsFormSlug" value="{settings.ha_itemsFormSlug}" placeholder="locations-materiel-2026">
93
+ </div>
94
+ </div>
95
+
96
+ <div class="mb-0 mt-3">
97
+ <label class="form-label">Préfixe itemName (checkout)</label>
98
+ <input class="form-control" type="text" name="ha_calendarItemNamePrefix" value="{settings.ha_calendarItemNamePrefix}">
99
+ </div>
100
+ </div>
101
+
102
+ <button class="btn btn-primary" type="submit">Enregistrer</button>
115
103
  </form>
116
- </div>
117
104
 
118
- <script>
119
- (function () {
120
- function ready(fn){ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); else fn(); }
121
- ready(function () {
122
- var btn = document.getElementById('ec-save');
123
- var form = document.getElementById('ec-admin-form');
124
- if (!btn || !form) { return; }
125
- btn.addEventListener('click', function () {
126
- try {
127
- form.submit();
128
- } catch (e) {
129
- console.error(e);
130
- alert('Erreur lors de la soumission du formulaire (voir console).');
131
- }
132
- });
133
- });
134
- }());
135
- </script>
105
+ {{{ if saved }}}
106
+ <script>
107
+ if (window.app && typeof window.app.alertSuccess === 'function') {
108
+ window.app.alertSuccess('Paramètres enregistrés');
109
+ }
110
+ </script>
111
+ {{{ end }}}
112
+ </div>
@@ -13,14 +13,60 @@
13
13
  <div id="equipment-calendar"></div>
14
14
  </div>
15
15
 
16
- <!-- Formulaire de soumission (utilisé par la modale JS) -->
17
- <form id="ec-create-form" method="post" action="/equipment/reservations/create" class="d-none">
18
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
19
- <input type="hidden" name="start" value="">
20
- <input type="hidden" name="end" value="">
21
- <input type="hidden" name="itemIds" value="">
22
- <input type="hidden" name="notesUser" value="">
23
- </form>
16
+ <!-- Modal création -->
17
+ <div class="modal fade" id="ec-create-modal" tabindex="-1" aria-hidden="true">
18
+ <div class="modal-dialog modal-dialog-centered">
19
+ <div class="modal-content">
20
+ <div class="modal-header">
21
+ <h5 class="modal-title">Nouvelle demande</h5>
22
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
23
+ </div>
24
+
25
+ <form id="ec-create-form" method="post" action="/equipment/reservations/create">
26
+ <div class="modal-body">
27
+ <input type="hidden" name="_csrf" value="{config.csrf_token}">
28
+ <input type="hidden" id="ec-start-ms" name="startMs" value="0">
29
+ <input type="hidden" id="ec-end-ms" name="endMs" value="0">
30
+
31
+ <div class="row g-3 mb-3">
32
+ <div class="col-6">
33
+ <label class="form-label">Début</label>
34
+ <input class="form-control" type="date" id="ec-start-date" required>
35
+ </div>
36
+ <div class="col-6">
37
+ <label class="form-label">Fin</label>
38
+ <input class="form-control" type="date" id="ec-end-date" required>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="mb-3">
43
+ <label class="form-label">Matériel</label>
44
+ <select class="form-select" id="ec-item-ids" name="itemIds" multiple required></select>
45
+ <div class="form-text">Tu peux sélectionner plusieurs matériels.</div>
46
+ </div>
47
+
48
+ <div class="mb-3">
49
+ <div class="fw-semibold">Durée</div>
50
+ <div id="ec-total-days">1 jour</div>
51
+ <hr class="my-2">
52
+ <div class="fw-semibold">Total estimé</div>
53
+ <div id="ec-total-price" class="fs-5">0 €</div>
54
+ </div>
55
+
56
+ <div class="mb-0">
57
+ <label class="form-label">Note</label>
58
+ <textarea class="form-control" name="notesUser" rows="3" placeholder="Optionnel"></textarea>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="modal-footer">
63
+ <button type="button" class="btn btn-light" data-bs-dismiss="modal">Annuler</button>
64
+ <button type="submit" class="btn btn-primary">Envoyer la demande</button>
65
+ </div>
66
+ </form>
67
+ </div>
68
+ </div>
69
+ </div>
24
70
  </div>
25
71
 
26
72
  <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js"></script>
@@ -31,12 +77,6 @@
31
77
  window.EC_BLOCKS = JSON.parse(atob('{blocksB64}'));
32
78
  window.EC_ITEMS = JSON.parse(atob('{itemsB64}'));
33
79
  window.EC_INITIAL_DATE = "{initialDateISO}";
34
- window.EC_INITIAL_VIEW = "{view}";
35
80
  window.EC_TZ = "{tz}";
36
81
  window.EC_CAN_CREATE = {canCreateJs};
37
82
  </script>
38
-
39
- <style>
40
- .ec-status-pending .fc-event-title { font-weight: 600; }
41
- .ec-status-valid .fc-event-title { font-weight: 700; }
42
- </style>