nodebb-plugin-equipment-calendar 0.9.9 → 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/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.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.9",
28
+ "version": "0.6.1",
29
29
  "minver": "4.7.1"
30
30
  }
@@ -2,6 +2,42 @@
2
2
  /* global window, document, FullCalendar, bootbox */
3
3
 
4
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
+
5
41
  function overlaps(aStartMs, aEndMs, bStartMs, bEndMs) {
6
42
  return aStartMs < bEndMs && aEndMs > bStartMs;
7
43
  }
@@ -20,8 +56,8 @@
20
56
 
21
57
  const blockedByItem = {};
22
58
  for (const b of blocks) {
23
- if (!blockedByItem[b.itemId]) blockedByItem[b.itemId] = [];
24
- blockedByItem[b.itemId].push(b);
59
+ if (!blockedByItem[b.itemIds]) blockedByItem[b.itemIds] = [];
60
+ blockedByItem[b.itemIds].push(b);
25
61
  }
26
62
 
27
63
  const available = [];
@@ -33,7 +69,7 @@
33
69
  return available;
34
70
  }
35
71
 
36
- function submitReservation(startISO, endISO, itemId, notes) {
72
+ function submitReservation(startISO, endISO, itemIds, notes) {
37
73
  const form = document.getElementById('ec-create-form');
38
74
  if (!form) return;
39
75
 
@@ -44,7 +80,7 @@
44
80
 
45
81
  if (startInput) startInput.value = startISO;
46
82
  if (endInput) endInput.value = endISO;
47
- if (itemInput) itemInput.value = itemId;
83
+ if (itemInput) itemInput.value = itemIds;
48
84
  if (notesInput) notesInput.value = notes || '';
49
85
 
50
86
  form.submit();
@@ -97,8 +133,8 @@
97
133
 
98
134
  if (typeof bootbox === 'undefined') {
99
135
  // Fallback without bootbox (should be rare on NodeBB pages)
100
- const itemId = available[0].id;
101
- submitReservation(startDate.toISOString(), endDate.toISOString(), itemId, '');
136
+ const itemIds = available[0].id;
137
+ submitReservation(startDate.toISOString(), endDate.toISOString(), itemIds, '');
102
138
  return;
103
139
  }
104
140
 
@@ -151,11 +187,10 @@
151
187
  },
152
188
 
153
189
  // 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());
190
+ dateClick: function(info) {
191
+ const startMs = toUtcMidnightMs(info.date);
192
+ const endMs = startMs + 24*60*60*1000;
193
+ openCreateModal(startMs, endMs);
159
194
  },
160
195
 
161
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>