nodebb-plugin-equipment-calendar 0.9.8 → 1.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/library.js CHANGED
@@ -700,6 +700,7 @@ async function renderAdminReservationsPage(req, res) {
700
700
  if (!(await ensureIsAdmin(req, res))) return;
701
701
 
702
702
  const settings = await getSettings();
703
+ const saved = false;
703
704
  const items = await getActiveItems(settings);
704
705
  const itemById = {};
705
706
  items.forEach(it => { itemById[it.id] = it; });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "0.9.8",
3
+ "version": "1.0.0",
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.7",
28
+ "version": "0.6.1",
29
29
  "minver": "4.7.1"
30
30
  }
@@ -1,33 +1,43 @@
1
-
2
- function updateTotalPrice() {
3
- try {
4
- const sel = document.getElementById('ec-item-ids');
5
- const out = document.getElementById('ec-total-price');
6
- const daysEl = document.getElementById('ec-total-days');
7
- if (!sel || !out) return;
8
- let total = 0;
9
- Array.from(sel.selectedOptions || []).forEach(opt => {
10
- const p = parseFloat(opt.getAttribute('data-price') || '0');
11
- if (!Number.isNaN(p)) total += p;
12
- });
13
- const days = getReservationDays();
14
- const finalTotal = total * days;
15
- if (daysEl) {
16
- daysEl.textContent = days + (days > 1 ? ' jours' : ' jour');
17
- }
18
- const txt = Number.isInteger(finalTotal) ? String(finalTotal) : finalTotal.toFixed(2);
19
- out.textContent = txt + ' €';
20
- } catch (e) {}
21
- });
22
- const txt = Number.isInteger(total) ? String(total) : total.toFixed(2);
23
- out.textContent = txt + ' €';
24
- } catch (e) {}
25
- }
26
-
27
1
  'use strict';
28
2
  /* global window, document, FullCalendar, bootbox */
29
3
 
30
4
  (function () {
5
+
6
+ function toUtcMidnightMs(dateObj) {
7
+ return Date.UTC(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
8
+ }
9
+
10
+ function formatDateInputValue(dateObj) {
11
+ // YYYY-MM-DD in UTC
12
+ const y = dateObj.getUTCFullYear();
13
+ const m = String(dateObj.getUTCMonth() + 1).padStart(2, '0');
14
+ const d = String(dateObj.getUTCDate()).padStart(2, '0');
15
+ return `${y}-${m}-${d}`;
16
+ }
17
+
18
+ function openCreateModal(startMs, endMs) {
19
+ const startDateEl = document.getElementById('ec-start-date');
20
+ const endDateEl = document.getElementById('ec-end-date');
21
+ const startMsEl = document.getElementById('ec-start-ms');
22
+ const endMsEl = document.getElementById('ec-end-ms');
23
+
24
+ if (startMsEl) startMsEl.value = String(startMs);
25
+ if (endMsEl) endMsEl.value = String(endMs);
26
+
27
+ // set date inputs
28
+ if (startDateEl) startDateEl.value = formatDateInputValue(new Date(startMs));
29
+ if (endDateEl) endDateEl.value = formatDateInputValue(new Date(endMs - 24*60*60*1000));
30
+
31
+ if (typeof updateTotalPrice === 'function') updateTotalPrice();
32
+
33
+ const modalEl = document.getElementById('ec-create-modal');
34
+ if (modalEl && window.bootstrap && window.bootstrap.Modal) {
35
+ const modal = window.bootstrap.Modal.getOrCreateInstance(modalEl);
36
+ modal.show();
37
+ }
38
+ }
39
+
40
+
31
41
  function overlaps(aStartMs, aEndMs, bStartMs, bEndMs) {
32
42
  return aStartMs < bEndMs && aEndMs > bStartMs;
33
43
  }
@@ -46,8 +56,8 @@
46
56
 
47
57
  const blockedByItem = {};
48
58
  for (const b of blocks) {
49
- if (!blockedByItem[b.itemId]) blockedByItem[b.itemId] = [];
50
- blockedByItem[b.itemId].push(b);
59
+ if (!blockedByItem[b.itemIds]) blockedByItem[b.itemIds] = [];
60
+ blockedByItem[b.itemIds].push(b);
51
61
  }
52
62
 
53
63
  const available = [];
@@ -59,7 +69,7 @@
59
69
  return available;
60
70
  }
61
71
 
62
- function submitReservation(startISO, endISO, itemId, notes) {
72
+ function submitReservation(startISO, endISO, itemIds, notes) {
63
73
  const form = document.getElementById('ec-create-form');
64
74
  if (!form) return;
65
75
 
@@ -70,7 +80,7 @@
70
80
 
71
81
  if (startInput) startInput.value = startISO;
72
82
  if (endInput) endInput.value = endISO;
73
- if (itemInput) itemInput.value = itemId;
83
+ if (itemInput) itemInput.value = itemIds;
74
84
  if (notesInput) notesInput.value = notes || '';
75
85
 
76
86
  form.submit();
@@ -123,8 +133,8 @@
123
133
 
124
134
  if (typeof bootbox === 'undefined') {
125
135
  // Fallback without bootbox (should be rare on NodeBB pages)
126
- const itemId = available[0].id;
127
- submitReservation(startDate.toISOString(), endDate.toISOString(), itemId, '');
136
+ const itemIds = available[0].id;
137
+ submitReservation(startDate.toISOString(), endDate.toISOString(), itemIds, '');
128
138
  return;
129
139
  }
130
140
 
@@ -177,11 +187,10 @@
177
187
  },
178
188
 
179
189
  // Single date click
180
- dateClick: function (info) {
181
- // Default: 1h slot
182
- const start = info.date;
183
- const end = new Date(start.getTime() + 60 * 60 * 1000);
184
- openNodeBBModal(start.toISOString(), end.toISOString());
190
+ dateClick: function(info) {
191
+ const startMs = toUtcMidnightMs(info.date);
192
+ const endMs = startMs + 24*60*60*1000;
193
+ openCreateModal(startMs, endMs);
185
194
  },
186
195
 
187
196
  headerToolbar: {
@@ -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>