nodebb-plugin-equipment-calendar 9.1.4 → 9.1.6

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
@@ -75,21 +75,30 @@ function toEvent(r) {
75
75
  plugin.init = async function (params) {
76
76
  const { router, middleware } = params;
77
77
 
78
+ // ACP
78
79
  router.get('/admin/plugins/equipment-calendar', middleware.admin.buildHeader, async (req, res) => {
79
80
  res.render('admin/plugins/equipment-calendar', {});
80
81
  });
81
82
  router.get('/api/admin/plugins/equipment-calendar', async (req, res) => res.json({}));
82
83
 
83
- router.get('/equipment/calendar', middleware.buildHeader, async (req, res) => {
84
- res.render('equipment-calendar/calendar', {});
85
- });
84
+ // Calendar page + alias /calendar
85
+ async function renderCal(req, res) {
86
+ const settings = await getSettings();
87
+ res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
88
+ }
89
+ router.get('/equipment/calendar', middleware.buildHeader, renderCal);
90
+ router.get('/calendar', middleware.buildHeader, renderCal);
91
+
86
92
  router.get('/api/equipment/calendar', async (req, res) => res.json({}));
93
+ router.get('/api/calendar', async (req, res) => res.json({}));
87
94
 
95
+ // Events
88
96
  router.get('/api/plugins/equipment-calendar/events', async (req, res) => {
89
97
  const list = await listReservations(1000);
90
98
  res.json({ events: list.map(toEvent) });
91
99
  });
92
100
 
101
+ // Create reservation
93
102
  router.post('/api/plugins/equipment-calendar/reservations', middleware.applyCSRF, async (req, res) => {
94
103
  const uid = req.uid;
95
104
  if (!uid) return res.status(403).json({ message: 'forbidden' });
@@ -109,7 +118,7 @@ plugin.init = async function (params) {
109
118
  res.json({ ok: true, rid: r.rid });
110
119
  });
111
120
 
112
- winston.info('[equipment-calendar] loaded (official4x standard bundle)');
121
+ winston.info('[equipment-calendar] loaded (official4x CDN fix)');
113
122
  };
114
123
 
115
124
  plugin.addAdminMenu = async function (header) {
package/package.json CHANGED
@@ -1,13 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "9.1.4",
4
- "description": "Equipment reservation calendar (NodeBB 4.x compatible, official ACP settings, FullCalendar standard bundle)",
3
+ "version": "9.1.6",
5
4
  "main": "library.js",
6
- "license": "MIT",
7
- "dependencies": {
8
- "fullcalendar": "^6.1.20"
9
- },
10
- "scripts": {
11
- "postinstall": "node ./scripts/postinstall.js"
12
- }
5
+ "license": "MIT"
13
6
  }
package/plugin.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "id": "nodebb-plugin-equipment-calendar",
3
3
  "name": "Equipment Calendar",
4
- "description": "Equipment reservation calendar (NodeBB 4.x)",
5
4
  "library": "./library.js",
6
5
  "hooks": [
7
6
  {
@@ -14,7 +13,7 @@
14
13
  }
15
14
  ],
16
15
  "acpScripts": [
17
- "public/js/acp.js"
16
+ "public/js/admin.js"
18
17
  ],
19
18
  "staticDirs": {
20
19
  "public": "public"
package/public/js/acp.js CHANGED
@@ -1,15 +1,15 @@
1
1
  'use strict';
2
- /* globals $, ajaxify */
3
- define('admin/plugins/equipment-calendar', ['settings', 'alerts'], function (Settings, alerts) {
4
- const Admin = {};
2
+ /* globals $, ajaxify, require */
5
3
 
6
- function onPage() {
4
+ (function () {
5
+ function onEnd(Settings, alerts) {
7
6
  const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : (window.location.pathname || '');
8
7
  if (!url.startsWith('admin/plugins/equipment-calendar')) return;
9
8
 
10
9
  const container = $('.equipment-calendar-settings');
11
10
  if (!container.length) return;
12
11
 
12
+ // load
13
13
  try {
14
14
  if (typeof Settings.sync === 'function') {
15
15
  Settings.sync('equipmentCalendar', container);
@@ -18,7 +18,7 @@ define('admin/plugins/equipment-calendar', ['settings', 'alerts'], function (Set
18
18
  }
19
19
  } catch (e) {}
20
20
 
21
- $('#save').off('click.ec').on('click.ec', function (e) {
21
+ $('#ec-save').off('click.ec').on('click.ec', function (e) {
22
22
  e.preventDefault();
23
23
  const done = function () {
24
24
  alerts.success('[[admin/settings:settings-saved]]');
@@ -35,10 +35,14 @@ define('admin/plugins/equipment-calendar', ['settings', 'alerts'], function (Set
35
35
  });
36
36
  }
37
37
 
38
- Admin.init = function () {
39
- $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', onPage);
40
- onPage();
41
- };
38
+ $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', function () {
39
+ require(['settings', 'alerts'], function (Settings, alerts) {
40
+ onEnd(Settings, alerts);
41
+ });
42
+ });
42
43
 
43
- return Admin;
44
- });
44
+ // run once
45
+ require(['settings', 'alerts'], function (Settings, alerts) {
46
+ onEnd(Settings, alerts);
47
+ });
48
+ })();
@@ -1,28 +1,37 @@
1
-
2
1
  'use strict';
3
2
  /* globals $, ajaxify */
4
3
  define('admin/plugins/equipment-calendar', ['settings', 'alerts'], function (Settings, alerts) {
5
4
  const Admin = {};
6
5
 
7
- function initOnPage() {
6
+ function onPage() {
8
7
  const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : (window.location.pathname || '');
9
- if (!url || url.indexOf('admin/plugins/equipment-calendar') !== 0) {
10
- return;
11
- }
8
+ if (!url.startsWith('admin/plugins/equipment-calendar')) return;
12
9
 
13
- Settings.load('equipmentCalendar', $('.acp-page-container'));
10
+ const container = $('.equipment-calendar-settings');
11
+ if (!container.length) return;
12
+
13
+ if (typeof Settings.sync === 'function') {
14
+ Settings.sync('equipmentCalendar', container);
15
+ } else {
16
+ Settings.load('equipmentCalendar', container);
17
+ }
14
18
 
15
19
  $('#save').off('click.ec').on('click.ec', function (e) {
16
- try { e.preventDefault(); } catch (err) {}
17
- Settings.save('equipmentCalendar', $('.acp-page-container'), function () {
20
+ e.preventDefault();
21
+ const done = function () {
18
22
  alerts.success('[[admin/settings:settings-saved]]');
19
- });
23
+ };
24
+ if (typeof Settings.persist === 'function') {
25
+ Settings.persist('equipmentCalendar', container, done, true);
26
+ } else {
27
+ Settings.save('equipmentCalendar', container, done);
28
+ }
20
29
  });
21
30
  }
22
31
 
23
32
  Admin.init = function () {
24
- $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', initOnPage);
25
- initOnPage();
33
+ $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', onPage);
34
+ onPage();
26
35
  };
27
36
 
28
37
  return Admin;
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
- /* globals $, ajaxify, FullCalendar */
2
+ /* globals $, ajaxify */
3
3
  define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
4
4
  const Client = {};
5
5
 
6
6
  function pageMatch() {
7
7
  const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : '';
8
- return url === 'equipment/calendar';
8
+ return url === 'equipment/calendar' || url === 'calendar';
9
9
  }
10
10
 
11
11
  function isoDate(s) { return String(s || '').slice(0, 10); }
@@ -29,7 +29,12 @@ define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
29
29
 
30
30
  async function initCalendar() {
31
31
  const el = document.getElementById('ec-calendar');
32
- if (!el || !window.FullCalendar || !window.FullCalendar.Calendar) return;
32
+ if (!el) return;
33
+
34
+ if (!window.FullCalendar || !window.FullCalendar.Calendar) {
35
+ el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN). Vérifie la CSP (Content-Security-Policy) et l’accès à cdn.jsdelivr.net.</div>';
36
+ return;
37
+ }
33
38
 
34
39
  const events = await loadEvents();
35
40
 
@@ -1,8 +1,5 @@
1
1
  <div class="container-fluid px-3 equipment-calendar-page">
2
- <div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
3
- <h1 class="mb-0">Calendrier des réservations</h1>
4
- </div>
5
-
2
+ <h1 class="mb-3">Calendrier des réservations</h1>
6
3
  <div id="ec-calendar"></div>
7
4
 
8
5
  <div class="modal fade" id="ecModal" tabindex="-1" aria-hidden="true">
@@ -24,7 +21,7 @@
24
21
  <input class="form-control" type="date" name="endIso" id="ec-end" required>
25
22
  </div>
26
23
  <div class="mb-3">
27
- <label class="form-label">Matériel (IDs, séparés par virgule) — placeholder (HelloAsso import à brancher)</label>
24
+ <label class="form-label">Matériel (IDs, séparés par virgule)</label>
28
25
  <input class="form-control" type="text" name="itemIds" id="ec-items" placeholder="ex: item123,item456" required>
29
26
  </div>
30
27
  </div>
@@ -38,8 +35,8 @@
38
35
  </div>
39
36
  </div>
40
37
 
41
- <link rel="stylesheet" href="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/index.global.min.css">
42
- <script src="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/index.global.min.js"></script>
38
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.css">
39
+ <script src="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js"></script>
43
40
 
44
41
  <script>
45
42
  require(['equipment-calendar/client'], function (mod) { mod.init(); });
package/README.md DELETED
@@ -1,43 +0,0 @@
1
- # NodeBB Equipment Calendar (v0.1.0)
2
-
3
- Plugin NodeBB (testé pour NodeBB v4.7.x) pour gérer des réservations de matériel via un calendrier (FullCalendar),
4
- avec workflow : demande -> validation par un groupe -> lien de paiement HelloAsso -> statut payé/validé.
5
-
6
- ## Fonctionnement (sans "AJAX applicatif")
7
- - La page calendrier est rendue côté serveur avec les évènements de la période demandée.
8
- - Les actions (création, validation, refus) sont des POST classiques, suivis d'une redirection.
9
- - FullCalendar est utilisé en mode "events inline" (pas de feed JSON automatique).
10
-
11
- ## Installation
12
- ```bash
13
- cd /path/to/nodebb
14
- npm install /path/to/nodebb-plugin-equipment-calendar
15
- ./nodebb build
16
- ./nodebb restart
17
- ```
18
-
19
- ## Configuration
20
- Dans l'ACP : Plugins -> Equipment Calendar
21
-
22
- - Groupes autorisés à créer une demande
23
- - Groupe validateur
24
- - Groupe notifié
25
- - Matériel (JSON)
26
- - Paramètres HelloAsso (clientId, clientSecret, organizationSlug, returnUrl, webhookSecret, etc.)
27
-
28
- ## Webhook HelloAsso
29
- Déclare l'URL :
30
- `https://<ton-forum>/equipment/webhook/helloasso`
31
-
32
- Le plugin vérifie la signature si `webhookSecret` est renseigné (exemple basique).
33
-
34
- ## Remarques
35
- - Ce plugin est un squelette complet mais générique : adapte la logique de paiement HelloAsso selon ton besoin exact
36
- (type de checkout, itemization, montant, etc.).
37
- - Pour un contrôle d'overlap strict : le plugin empêche les réservations qui chevauchent (même item) pour les statuts bloquants.
38
-
39
-
40
- ## URLs
41
- - Calendrier: `/equipment/calendar` (alias `/calendar`)
42
- - Validations: `/equipment/approvals`
43
- - ACP: `/admin/plugins/equipment-calendar`
@@ -1,2 +0,0 @@
1
- .equipment-calendar-page {}
2
- .ec-status-pending {}
@@ -1,32 +0,0 @@
1
- 'use strict';
2
- /* global $, app */
3
-
4
- define('admin/plugins/equipment-calendar', function () {
5
- const EquipmentCalendar = {};
6
-
7
- EquipmentCalendar.init = function () {
8
- $('#save').on('click', function () {
9
- const payload = {
10
- creatorGroups: $('#creatorGroups').val(),
11
- approverGroup: $('#approverGroup').val(),
12
- notifyGroup: $('#notifyGroup').val(),
13
- itemsJson: $('#itemsJson').val(),
14
- ha_clientId: $('#ha_clientId').val(),
15
- ha_clientSecret: $('#ha_clientSecret').val(),
16
- ha_organizationSlug: $('#ha_organizationSlug').val(),
17
- ha_returnUrl: $('#ha_returnUrl').val(),
18
- ha_webhookSecret: $('#ha_webhookSecret').val(),
19
- defaultView: $('#defaultView').val(),
20
- timezone: $('#timezone').val(),
21
- showRequesterToAll: $('#showRequesterToAll').is(':checked') ? '1' : '0',
22
- };
23
-
24
- socket.emit('admin.settings.save', { hash: 'equipmentCalendar', values: payload }, function (err) {
25
- if (err) return app.alertError(err.message || err);
26
- app.alertSuccess('Sauvegardé');
27
- });
28
- });
29
- };
30
-
31
- return EquipmentCalendar;
32
- });
@@ -1,50 +0,0 @@
1
- 'use strict';
2
- /* global $, window, document, FullCalendar */
3
-
4
- (function () {
5
- function submitCreate(startISO, endISO) {
6
- const form = document.getElementById('ec-create-form');
7
- if (!form) return;
8
- form.querySelector('input[name="start"]').value = startISO;
9
- form.querySelector('input[name="end"]').value = endISO;
10
- form.submit();
11
- }
12
-
13
- $(document).ready(function () {
14
- const el = document.getElementById('equipment-calendar');
15
- if (!el) return;
16
-
17
- const events = window.EC_EVENTS || [];
18
- const initialDate = window.EC_INITIAL_DATE;
19
- const initialView = window.EC_INITIAL_VIEW || 'dayGridMonth';
20
-
21
- const calendar = new FullCalendar.Calendar(el, {
22
- initialView: initialView,
23
- initialDate: initialDate,
24
- timeZone: window.EC_TZ || 'local',
25
- selectable: window.EC_CAN_CREATE === true,
26
- selectMirror: true,
27
- events: events,
28
- select: function (info) {
29
- // FullCalendar provides end exclusive for all-day; for timed selections it's fine.
30
- submitCreate(info.startStr, info.endStr);
31
- },
32
- dateClick: function (info) {
33
- // Create 1-hour slot by default
34
- const start = info.date;
35
- const end = new Date(start.getTime() + 60 * 60 * 1000);
36
- submitCreate(start.toISOString(), end.toISOString());
37
- },
38
- eventClick: function (info) {
39
- // optionally show details
40
- },
41
- headerToolbar: {
42
- left: 'prev,next today',
43
- center: 'title',
44
- right: 'dayGridMonth,timeGridWeek,timeGridDay'
45
- },
46
- });
47
-
48
- calendar.render();
49
- });
50
- }());
package/public/styles.css DELETED
@@ -1 +0,0 @@
1
- .equipment-calendar-page { }
@@ -1,54 +0,0 @@
1
- <div class="acp-page-container">
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-outline-secondary" 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-secondary active" href="/admin/plugins/equipment-calendar/helloasso-test">Test HelloAsso</a>
8
- </div>
9
- </div>
10
-
11
- <div class="card card-body mt-3">
12
- <div class="d-flex flex-wrap gap-2 mb-2">
13
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test">Test (cache OK)</a>
14
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test?force=1">Test (forcer nouveau token)</a>
15
- <a class="btn btn-outline-danger" href="/admin/plugins/equipment-calendar/helloasso-test?force=1&clear=1" onclick="return confirm('Supprimer le token stocké et retester ?');">Vider token + retester</a>
16
- </div>
17
- {{{ if ok }}}
18
- <div class="alert alert-success">{message}</div>
19
- {{{ else }}}
20
- <div class="alert alert-danger">Échec : {message}</div>
21
- {{{ end }}}
22
-
23
- <div class="small text-muted">
24
- Form: <code>{settings.ha_itemsFormType}</code> / <code>{settings.ha_itemsFormSlug}</code> — Orga: <code>{settings.ha_organizationSlug}</code>
25
- </div>
26
- {{{ if hasSampleItems }}}
27
- <div class="card card-body mt-3">
28
- <div class="d-flex flex-wrap gap-2 mb-2">
29
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test">Test (cache OK)</a>
30
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test?force=1">Test (forcer nouveau token)</a>
31
- <a class="btn btn-outline-danger" href="/admin/plugins/equipment-calendar/helloasso-test?force=1&clear=1" onclick="return confirm('Supprimer le token stocké et retester ?');">Vider token + retester</a>
32
- </div>
33
- <h5 class="mb-2">Aperçu (10 premiers articles)</h5>
34
- <div class="table-responsive">
35
- <table class="table table-striped align-middle">
36
- <thead>
37
- <tr>
38
- <th>ID</th>
39
- <th>Nom</th>
40
- </tr>
41
- </thead>
42
- <tbody>
43
- {{{ each sampleItems }}}
44
- <tr>
45
- <td><code>{sampleItems.id}</code></td>
46
- <td>{sampleItems.rawName}</td>
47
- </tr>
48
- {{{ end }}}
49
- </tbody>
50
- </table>
51
- </div>
52
- </div>
53
- {{{ end }}}
54
- </div>
@@ -1,99 +0,0 @@
1
- <div class="acp-page-container">
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-outline-secondary" href="/admin/plugins/equipment-calendar">Paramètres</a>
6
- <a class="btn btn-secondary active" href="/admin/plugins/equipment-calendar/reservations">Réservations</a>
7
- </div>
8
- </div>
9
-
10
- <form method="get" action="/admin/plugins/equipment-calendar/reservations" class="card card-body mt-3 mb-3">
11
- <div class="row g-2 align-items-end">
12
- <div class="col-md-3">
13
- <label class="form-label">Statut</label>
14
- <select class="form-select" name="status">
15
- {{{ each statusOptions }}}
16
- <option value="{statusOptions.id}" {{{ if statusOptions.selected }}}selected{{{ end }}}>{statusOptions.name}</option>
17
- {{{ end }}}
18
- </select>
19
- </div>
20
- <div class="col-md-4">
21
- <label class="form-label">Matériel</label>
22
- <select class="form-select" name="itemId">
23
- {{{ each itemOptions }}}
24
- <option value="{itemOptions.id}" {{{ if itemOptions.selected }}}selected{{{ end }}}>{itemOptions.name}</option>
25
- {{{ end }}}
26
- </select>
27
- </div>
28
- <div class="col-md-3">
29
- <label class="form-label">Recherche</label>
30
- <input class="form-control" name="q" value="{q}" placeholder="rid, uid, note">
31
- </div>
32
- <div class="col-md-2">
33
- <button class="btn btn-primary w-100" type="submit">Filtrer</button>
34
- </div>
35
- </div>
36
- </form>
37
-
38
- <div class="text-muted small mb-2">
39
- Total (filtré) : <strong>{total}</strong> - Total (global) : <strong>{totalAll}</strong>
40
- </div>
41
-
42
- {{{ if hasRows }}}
43
- <div class="table-responsive">
44
- <table class="table table-striped align-middle">
45
- <thead><tr>
46
- <th>Matériel</th>
47
- <th>Période</th>
48
- <th>Créée le</th>
49
- <th>Statut</th>
50
- <th>Total</th>
51
- <th class="text-end">Actions</th>
52
- </tr></thead>
53
- <tbody>
54
- {{{ each rows }}}
55
- <tr>
56
- <td><code>{rows.rid}</code></td>
57
- <td>{rows.itemName}</td>
58
- <td>{rows.uid}</td>
59
- <td><small>{rows.start}</small></td>
60
- <td><small>{rows.end}</small></td>
61
- <td><code>{rows.status}</code></td>
62
- <td><small>{rows.createdAt}</small></td>
63
- <td><small>{rows.notesUser}</small></td>
64
- <td class="text-nowrap">
65
- <form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.rid}/approve" class="d-inline">
66
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
67
- <button class="btn btn-sm btn-success" type="submit">Approve</button>
68
- </form>
69
- <form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.rid}/reject" class="d-inline ms-1">
70
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
71
- <button class="btn btn-sm btn-warning" type="submit">Reject</button>
72
- </form>
73
- <form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.rid}/delete" class="d-inline ms-1" onsubmit="return confirm('Supprimer définitivement ?');">
74
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
75
- <button class="btn btn-sm btn-danger" type="submit">Delete</button>
76
- </form>
77
- </td>
78
- </tr>
79
- {{{ end }}}
80
- </tbody>
81
- </table>
82
- </div>
83
-
84
- <div class="d-flex justify-content-between align-items-center mt-3">
85
- <div>Page {page} / {totalPages}</div>
86
- <div class="btn-group">
87
- {{{ if prevPage }}}
88
- <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations?page={prevPage}&perPage={perPage}">Précédent</a>
89
- {{{ end }}}
90
- {{{ if nextPage }}}
91
- <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations?page={nextPage}&perPage={perPage}">Suivant</a>
92
- {{{ end }}}
93
- </div>
94
- </div>
95
-
96
- {{{ else }}}
97
- <div class="alert alert-info">Aucune réservation trouvée.</div>
98
- {{{ end }}}
99
- </div>
@@ -1,83 +0,0 @@
1
- <div class="acp-page-container">
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>
8
- </div>
9
- </div>
10
-
11
- <form id="ec-admin-form" method="post" action="/admin/plugins/equipment-calendar/save" class="mb-3 mt-3">
12
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
13
-
14
- <div class="card card-body mb-3">
15
- <h5>Permissions</h5>
16
- <div class="mb-3">
17
- <label class="form-label">Groupes autorisés à créer une demande (creatorGroups)</label>
18
- <input class="form-control" type="text" name="creatorGroups" value="{settings.creatorGroups}">
19
- </div>
20
- <div class="mb-3">
21
- <label class="form-label">Groupe valideur (approverGroup)</label>
22
- <input class="form-control" type="text" name="approverGroup" value="{settings.approverGroup}">
23
- </div>
24
- <div class="mb-0">
25
- <label class="form-label">Groupe notifié (notifyGroup)</label>
26
- <input class="form-control" type="text" name="notifyGroup" value="{settings.notifyGroup}">
27
- </div>
28
- </div>
29
-
30
- <div class="card card-body mb-3">
31
- <h5>Paiement</h5>
32
- <div class="mb-0">
33
- <label class="form-label">Timeout paiement (minutes)</label>
34
- <input class="form-control" type="number" min="1" name="paymentTimeoutMinutes" value="{settings.paymentTimeoutMinutes}">
35
- </div>
36
- </div>
37
-
38
- <div class="card card-body mb-3">
39
- <h5>HelloAsso</h5>
40
- <div class="mb-3">
41
- <label class="form-label">API Base URL (prod/sandbox)</label>
42
- <input class="form-control" type="text" name="ha_apiBaseUrl" value="{settings.ha_apiBaseUrl}">
43
- <div class="form-text">Prod: <code>https://api.helloasso.com</code> — Sandbox: <code>https://api.helloasso-sandbox.com</code></div>
44
- </div>
45
- <div class="mb-3">
46
- <label class="form-label">Organization slug</label>
47
- <input class="form-control" type="text" name="ha_organizationSlug" value="{settings.ha_organizationSlug}">
48
- </div>
49
- <div class="mb-3">
50
- <label class="form-label">Client ID</label>
51
- <input class="form-control" type="text" name="ha_clientId" value="{settings.ha_clientId}">
52
- </div>
53
- <div class="mb-3">
54
- <label class="form-label">Client Secret</label>
55
- <input class="form-control" type="password" name="ha_clientSecret" value="{settings.ha_clientSecret}">
56
- </div>
57
- <div class="row g-3">
58
- <div class="col-md-6">
59
- <label class="form-label">Form Type</label>
60
- <input class="form-control" type="text" name="ha_itemsFormType" value="{settings.ha_itemsFormType}" placeholder="shop">
61
- </div>
62
- <div class="col-md-6">
63
- <label class="form-label">Form Slug</label>
64
- <input class="form-control" type="text" name="ha_itemsFormSlug" value="{settings.ha_itemsFormSlug}" placeholder="locations-materiel-2026">
65
- </div>
66
- </div>
67
- <div class="mb-0 mt-3">
68
- <label class="form-label">Préfixe itemName (checkout)</label>
69
- <input class="form-control" type="text" name="ha_calendarItemNamePrefix" value="{settings.ha_calendarItemNamePrefix}">
70
- </div>
71
- </div>
72
-
73
- <button class="btn btn-primary" type="submit">Enregistrer</button>
74
- </form>
75
-
76
- {{{ if saved }}}
77
- <script>
78
- if (window.app && typeof window.app.alertSuccess === 'function') {
79
- window.app.alertSuccess('Paramètres enregistrés');
80
- }
81
- </script>
82
- {{{ end }}}
83
- </div>
@@ -1,51 +0,0 @@
1
- <div class="equipment-approvals-page">
2
- <h1>Validation des réservations</h1>
3
-
4
- {{{ if hasRows }}}
5
- <div class="table-responsive">
6
- <table class="table table-striped align-middle">
7
- <thead>
8
- <tr>
9
- <th>Matériel</th>
10
- <th>Demandeur</th>
11
- <th>Début</th>
12
- <th>Fin</th>
13
- <th>Statut</th>
14
- <th>Paiement</th>
15
- <th>Actions</th>
16
- </tr>
17
- </thead>
18
- <tbody>
19
- {{{ each rows }}}
20
- <tr>
21
- <td>{rows.itemName}</td>
22
- <td>{rows.requester}</td>
23
- <td>{rows.start}</td>
24
- <td>{rows.end}</td>
25
- <td><code>{rows.status}</code></td>
26
- <td>
27
- {{{ if rows.paymentUrl }}}
28
- <a href="{rows.paymentUrl}" target="_blank" rel="noreferrer">Lien</a>
29
- {{{ else }}}
30
- -
31
- {{{ end }}}
32
- </td>
33
- <td>
34
- <form method="post" action="/equipment/reservations/{rows.id}/approve" class="d-inline">
35
- <input type="hidden" name="_csrf" value="{rows.csrf}">
36
- <button class="btn btn-sm btn-success" type="submit">Approuver</button>
37
- </form>
38
- <form method="post" action="/equipment/reservations/{rows.id}/reject" class="d-inline ms-1">
39
- <input type="hidden" name="_csrf" value="{rows.csrf}">
40
- <button class="btn btn-sm btn-danger" type="submit">Refuser</button>
41
- </form>
42
- </td>
43
- </tr>
44
- {{{ end }}}
45
- </tbody>
46
- </table>
47
- </div>
48
- {{{ else }}}
49
- <div class="alert alert-success">Aucune demande en attente 🎉</div>
50
- {{{ end }}}
51
- </div>
@@ -1,89 +0,0 @@
1
- <div class="equipment-calendar-page">
2
- <h1>Réservation de matériel</h1>
3
-
4
- {{{ if canCreate }}}
5
- <div class="alert alert-info">
6
- Clique sur une date ou sélectionne une plage sur le calendrier pour faire une demande.
7
- </div>
8
- {{{ else }}}
9
- <div class="alert alert-info">Tu peux consulter le calendrier, mais tu n’as pas les droits pour créer une demande.</div>
10
- {{{ end }}}
11
-
12
- <div class="card card-body mb-3">
13
- <div id="ec-calendar"></div>
14
- </div>
15
-
16
- <!-- Modal de demande -->
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
- <form id="ec-create-form" method="post" action="{relative_path}/equipment/reservations/create">
21
- <div class="modal-header">
22
- <h5 class="modal-title">Demande de réservation</h5>
23
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
24
- </div>
25
-
26
- <div class="modal-body">
27
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
28
- <input type="hidden" id="ec-start-iso" name="start" value="">
29
- <input type="hidden" id="ec-end-iso" name="end" value="">
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>
45
- <!-- BEGIN items -->
46
- <option value="{items.id}" data-price="{items.price}">{items.name}</option>
47
- <!-- END items -->
48
- </select>
49
- <div class="form-text">Tu peux sélectionner plusieurs matériels.</div>
50
- </div>
51
-
52
- <div class="mb-3">
53
- <label class="form-label">Notes</label>
54
- <textarea class="form-control" name="notesUser" rows="3" placeholder="Infos utiles..."></textarea>
55
- </div>
56
-
57
- <div class="mb-0">
58
- <div class="fw-semibold">Durée</div>
59
- <div id="ec-total-days">1 jour</div>
60
- <hr class="my-2">
61
- <div class="fw-semibold">Total estimé</div>
62
- <div id="ec-total-price" class="fs-5">0 EUR</div>
63
- </div>
64
- </div>
65
-
66
- <div class="modal-footer">
67
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
68
- <button type="submit" class="btn btn-primary">Envoyer la demande</button>
69
- </div>
70
- </form>
71
- </div>
72
- </div>
73
- </div>
74
- </div>
75
-
76
- <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.19/index.global.min.js"></script>
77
- <script src="{relative_path}/plugins/nodebb-plugin-equipment-calendar/js/client.js"></script>
78
-
79
- <script>
80
- window.EC_EVENTS = JSON.parse(atob('{eventsB64}'));
81
- window.EC_BLOCKS = JSON.parse(atob('{blocksB64}'));
82
- window.EC_CAN_CREATE = {canCreateJs};
83
- window.EC_TZ = '{tz}';
84
- </script>
85
-
86
- <script>
87
- window.EC_CSRF = '{csrf_token}';
88
- window.EC_IS_APPROVER = {isApprover};
89
- </script>
@@ -1,15 +0,0 @@
1
- <div class="card card-body">
2
- <h4>Retour paiement</h4>
3
-
4
- {{{ if statusPaid }}}
5
- <div class="alert alert-success">✅ Paiement confirmé.</div>
6
- {{{ end }}}
7
-
8
- {{{ if statusError }}}
9
- <div class="alert alert-danger">❌ Paiement non confirmé.</div>
10
- {{{ end }}}
11
-
12
- <p>{message}</p>
13
-
14
- <a class="btn btn-primary" href="/equipment/calendar">Retour au calendrier</a>
15
- </div>
@@ -1,25 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function copyFile(src, dst) {
7
- fs.mkdirSync(path.dirname(dst), { recursive: true });
8
- fs.copyFileSync(src, dst);
9
- }
10
- function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
11
-
12
- (function main() {
13
- const root = path.join(__dirname, '..');
14
- const outDir = path.join(root, 'public', 'vendor', 'fullcalendar');
15
- fs.mkdirSync(outDir, { recursive: true });
16
-
17
- const js = path.join(root, 'node_modules', 'fullcalendar', 'index.global.min.js');
18
- const css = path.join(root, 'node_modules', 'fullcalendar', 'index.global.min.css');
19
-
20
- const copied = [];
21
- if (exists(js)) { copyFile(js, path.join(outDir, 'index.global.min.js')); copied.push(js); }
22
- if (exists(css)) { copyFile(css, path.join(outDir, 'index.global.min.css')); copied.push(css); }
23
-
24
- fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify({ copied }, null, 2), 'utf-8');
25
- })();
@@ -1,54 +0,0 @@
1
- <div class="acp-page-container">
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-outline-secondary" 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-secondary active" href="/admin/plugins/equipment-calendar/helloasso-test">Test HelloAsso</a>
8
- </div>
9
- </div>
10
-
11
- <div class="card card-body mt-3">
12
- <div class="d-flex flex-wrap gap-2 mb-2">
13
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test">Test (cache OK)</a>
14
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test?force=1">Test (forcer nouveau token)</a>
15
- <a class="btn btn-outline-danger" href="/admin/plugins/equipment-calendar/helloasso-test?force=1&clear=1" onclick="return confirm('Supprimer le token stocké et retester ?');">Vider token + retester</a>
16
- </div>
17
- {{{ if ok }}}
18
- <div class="alert alert-success">{message}</div>
19
- {{{ else }}}
20
- <div class="alert alert-danger">Échec : {message}</div>
21
- {{{ end }}}
22
-
23
- <div class="small text-muted">
24
- Form: <code>{settings.ha_itemsFormType}</code> / <code>{settings.ha_itemsFormSlug}</code> — Orga: <code>{settings.ha_organizationSlug}</code>
25
- </div>
26
- {{{ if hasSampleItems }}}
27
- <div class="card card-body mt-3">
28
- <div class="d-flex flex-wrap gap-2 mb-2">
29
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test">Test (cache OK)</a>
30
- <a class="btn btn-outline-primary" href="/admin/plugins/equipment-calendar/helloasso-test?force=1">Test (forcer nouveau token)</a>
31
- <a class="btn btn-outline-danger" href="/admin/plugins/equipment-calendar/helloasso-test?force=1&clear=1" onclick="return confirm('Supprimer le token stocké et retester ?');">Vider token + retester</a>
32
- </div>
33
- <h5 class="mb-2">Aperçu (10 premiers articles)</h5>
34
- <div class="table-responsive">
35
- <table class="table table-striped align-middle">
36
- <thead>
37
- <tr>
38
- <th>ID</th>
39
- <th>Nom</th>
40
- </tr>
41
- </thead>
42
- <tbody>
43
- {{{ each sampleItems }}}
44
- <tr>
45
- <td><code>{sampleItems.id}</code></td>
46
- <td>{sampleItems.rawName}</td>
47
- </tr>
48
- {{{ end }}}
49
- </tbody>
50
- </table>
51
- </div>
52
- </div>
53
- {{{ end }}}
54
- </div>
@@ -1,99 +0,0 @@
1
- <div class="acp-page-container">
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-outline-secondary" href="/admin/plugins/equipment-calendar">Paramètres</a>
6
- <a class="btn btn-secondary active" href="/admin/plugins/equipment-calendar/reservations">Réservations</a>
7
- </div>
8
- </div>
9
-
10
- <form method="get" action="/admin/plugins/equipment-calendar/reservations" class="card card-body mt-3 mb-3">
11
- <div class="row g-2 align-items-end">
12
- <div class="col-md-3">
13
- <label class="form-label">Statut</label>
14
- <select class="form-select" name="status">
15
- {{{ each statusOptions }}}
16
- <option value="{statusOptions.id}" {{{ if statusOptions.selected }}}selected{{{ end }}}>{statusOptions.name}</option>
17
- {{{ end }}}
18
- </select>
19
- </div>
20
- <div class="col-md-4">
21
- <label class="form-label">Matériel</label>
22
- <select class="form-select" name="itemId">
23
- {{{ each itemOptions }}}
24
- <option value="{itemOptions.id}" {{{ if itemOptions.selected }}}selected{{{ end }}}>{itemOptions.name}</option>
25
- {{{ end }}}
26
- </select>
27
- </div>
28
- <div class="col-md-3">
29
- <label class="form-label">Recherche</label>
30
- <input class="form-control" name="q" value="{q}" placeholder="rid, uid, note">
31
- </div>
32
- <div class="col-md-2">
33
- <button class="btn btn-primary w-100" type="submit">Filtrer</button>
34
- </div>
35
- </div>
36
- </form>
37
-
38
- <div class="text-muted small mb-2">
39
- Total (filtré) : <strong>{total}</strong> - Total (global) : <strong>{totalAll}</strong>
40
- </div>
41
-
42
- {{{ if hasRows }}}
43
- <div class="table-responsive">
44
- <table class="table table-striped align-middle">
45
- <thead><tr>
46
- <th>Matériel</th>
47
- <th>Période</th>
48
- <th>Créée le</th>
49
- <th>Statut</th>
50
- <th>Total</th>
51
- <th class="text-end">Actions</th>
52
- </tr></thead>
53
- <tbody>
54
- {{{ each rows }}}
55
- <tr>
56
- <td><code>{rows.rid}</code></td>
57
- <td>{rows.itemName}</td>
58
- <td>{rows.uid}</td>
59
- <td><small>{rows.start}</small></td>
60
- <td><small>{rows.end}</small></td>
61
- <td><code>{rows.status}</code></td>
62
- <td><small>{rows.createdAt}</small></td>
63
- <td><small>{rows.notesUser}</small></td>
64
- <td class="text-nowrap">
65
- <form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.rid}/approve" class="d-inline">
66
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
67
- <button class="btn btn-sm btn-success" type="submit">Approve</button>
68
- </form>
69
- <form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.rid}/reject" class="d-inline ms-1">
70
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
71
- <button class="btn btn-sm btn-warning" type="submit">Reject</button>
72
- </form>
73
- <form method="post" action="/admin/plugins/equipment-calendar/reservations/{rows.rid}/delete" class="d-inline ms-1" onsubmit="return confirm('Supprimer définitivement ?');">
74
- <input type="hidden" name="_csrf" value="{config.csrf_token}">
75
- <button class="btn btn-sm btn-danger" type="submit">Delete</button>
76
- </form>
77
- </td>
78
- </tr>
79
- {{{ end }}}
80
- </tbody>
81
- </table>
82
- </div>
83
-
84
- <div class="d-flex justify-content-between align-items-center mt-3">
85
- <div>Page {page} / {totalPages}</div>
86
- <div class="btn-group">
87
- {{{ if prevPage }}}
88
- <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations?page={prevPage}&perPage={perPage}">Précédent</a>
89
- {{{ end }}}
90
- {{{ if nextPage }}}
91
- <a class="btn btn-outline-secondary" href="/admin/plugins/equipment-calendar/reservations?page={nextPage}&perPage={perPage}">Suivant</a>
92
- {{{ end }}}
93
- </div>
94
- </div>
95
-
96
- {{{ else }}}
97
- <div class="alert alert-info">Aucune réservation trouvée.</div>
98
- {{{ end }}}
99
- </div>
@@ -1,51 +0,0 @@
1
- <div class="equipment-approvals-page">
2
- <h1>Validation des réservations</h1>
3
-
4
- {{{ if hasRows }}}
5
- <div class="table-responsive">
6
- <table class="table table-striped align-middle">
7
- <thead>
8
- <tr>
9
- <th>Matériel</th>
10
- <th>Demandeur</th>
11
- <th>Début</th>
12
- <th>Fin</th>
13
- <th>Statut</th>
14
- <th>Paiement</th>
15
- <th>Actions</th>
16
- </tr>
17
- </thead>
18
- <tbody>
19
- {{{ each rows }}}
20
- <tr>
21
- <td>{rows.itemName}</td>
22
- <td>{rows.requester}</td>
23
- <td>{rows.start}</td>
24
- <td>{rows.end}</td>
25
- <td><code>{rows.status}</code></td>
26
- <td>
27
- {{{ if rows.paymentUrl }}}
28
- <a href="{rows.paymentUrl}" target="_blank" rel="noreferrer">Lien</a>
29
- {{{ else }}}
30
- -
31
- {{{ end }}}
32
- </td>
33
- <td>
34
- <form method="post" action="/equipment/reservations/{rows.id}/approve" class="d-inline">
35
- <input type="hidden" name="_csrf" value="{rows.csrf}">
36
- <button class="btn btn-sm btn-success" type="submit">Approuver</button>
37
- </form>
38
- <form method="post" action="/equipment/reservations/{rows.id}/reject" class="d-inline ms-1">
39
- <input type="hidden" name="_csrf" value="{rows.csrf}">
40
- <button class="btn btn-sm btn-danger" type="submit">Refuser</button>
41
- </form>
42
- </td>
43
- </tr>
44
- {{{ end }}}
45
- </tbody>
46
- </table>
47
- </div>
48
- {{{ else }}}
49
- <div class="alert alert-success">Aucune demande en attente 🎉</div>
50
- {{{ end }}}
51
- </div>
@@ -1,15 +0,0 @@
1
- <div class="card card-body">
2
- <h4>Retour paiement</h4>
3
-
4
- {{{ if statusPaid }}}
5
- <div class="alert alert-success">✅ Paiement confirmé.</div>
6
- {{{ end }}}
7
-
8
- {{{ if statusError }}}
9
- <div class="alert alert-danger">❌ Paiement non confirmé.</div>
10
- {{{ end }}}
11
-
12
- <p>{message}</p>
13
-
14
- <a class="btn btn-primary" href="/equipment/calendar">Retour au calendrier</a>
15
- </div>