nodebb-plugin-equipment-calendar 9.1.3 → 9.1.5

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
@@ -1,9 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const nconf = require.main.require('nconf');
4
3
  const meta = require.main.require('./src/meta');
5
4
  const db = require.main.require('./src/database');
6
- const user = require.main.require('./src/user');
7
5
  const groups = require.main.require('./src/groups');
8
6
  const winston = require.main.require('winston');
9
7
 
@@ -35,16 +33,13 @@ async function getSettings() {
35
33
  async function isUserInAnyGroups(uid, groupCsv) {
36
34
  if (!uid || !groupCsv) return false;
37
35
  const names = String(groupCsv).split(',').map(s => s.trim()).filter(Boolean);
38
- if (!names.length) return false;
39
36
  for (const name of names) {
40
- const inGroup = await groups.isMember(uid, name);
41
- if (inGroup) return true;
37
+ if (await groups.isMember(uid, name)) return true;
42
38
  }
43
39
  return false;
44
40
  }
45
41
 
46
- function rid() {
47
- // UUID v4 (no dependency)
42
+ function genRid() {
48
43
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
49
44
  const r = Math.random() * 16 | 0;
50
45
  const v = c === 'x' ? r : (r & 0x3 | 0x8);
@@ -60,14 +55,13 @@ async function saveReservation(r) {
60
55
  await db.sortedSetAdd(reservationsZset, r.createdAt || Date.now(), r.rid);
61
56
  }
62
57
 
63
- async function listReservations(limit=500) {
58
+ async function listReservations(limit=1000) {
64
59
  const rids = await db.getSortedSetRevRange(reservationsZset, 0, limit - 1);
65
60
  const objs = await Promise.all(rids.map(id => db.getObject(reservationKey(id))));
66
61
  return objs.filter(Boolean);
67
62
  }
68
63
 
69
64
  function toEvent(r) {
70
- // FullCalendar allDay uses exclusive end
71
65
  const start = String(r.startIso).slice(0,10);
72
66
  const end = new Date(String(r.endIso).slice(0,10) + 'T00:00:00Z');
73
67
  end.setUTCDate(end.getUTCDate() + 1);
@@ -75,42 +69,28 @@ function toEvent(r) {
75
69
 
76
70
  const status = r.status || 'pending';
77
71
  const title = status === 'paid' ? '[PAID] Reservation' : (status === 'approved' ? '[APPROVED] Reservation' : '[PENDING] Reservation');
78
-
79
- return {
80
- id: r.rid,
81
- title,
82
- start,
83
- end: endExcl,
84
- allDay: true,
85
- };
72
+ return { id: r.rid, title, start, end: endExcl, allDay: true };
86
73
  }
87
74
 
88
75
  plugin.init = async function (params) {
89
76
  const { router, middleware } = params;
90
77
 
91
- // Admin page
92
78
  router.get('/admin/plugins/equipment-calendar', middleware.admin.buildHeader, async (req, res) => {
93
79
  res.render('admin/plugins/equipment-calendar', {});
94
80
  });
95
- router.get('/api/admin/plugins/equipment-calendar', async (req, res) => {
96
- res.json({});
97
- });
81
+ router.get('/api/admin/plugins/equipment-calendar', async (req, res) => res.json({}));
98
82
 
99
- // Calendar page
100
83
  router.get('/equipment/calendar', middleware.buildHeader, async (req, res) => {
101
- res.render('equipment-calendar/calendar', {});
102
- });
103
- router.get('/api/equipment/calendar', async (req, res) => {
104
- res.json({});
84
+ const settings = await getSettings();
85
+ res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
105
86
  });
87
+ router.get('/api/equipment/calendar', async (req, res) => res.json({}));
106
88
 
107
- // API: events
108
89
  router.get('/api/plugins/equipment-calendar/events', async (req, res) => {
109
90
  const list = await listReservations(1000);
110
91
  res.json({ events: list.map(toEvent) });
111
92
  });
112
93
 
113
- // API: create reservation
114
94
  router.post('/api/plugins/equipment-calendar/reservations', middleware.applyCSRF, async (req, res) => {
115
95
  const uid = req.uid;
116
96
  if (!uid) return res.status(403).json({ message: 'forbidden' });
@@ -125,36 +105,17 @@ plugin.init = async function (params) {
125
105
  if (!startIso || !endIso) return res.status(400).json({ message: 'dates required' });
126
106
  if (!itemIds.length) return res.status(400).json({ message: 'items required' });
127
107
 
128
- const r = {
129
- rid: rid(),
130
- uid,
131
- startIso,
132
- endIso,
133
- itemIds: itemIds.map(String),
134
- status: 'pending',
135
- createdAt: Date.now(),
136
- };
108
+ const r = { rid: genRid(), uid, startIso, endIso, itemIds: itemIds.map(String), status: 'pending', createdAt: Date.now() };
137
109
  await saveReservation(r);
138
110
  res.json({ ok: true, rid: r.rid });
139
111
  });
140
112
 
141
- winston.info('[equipment-calendar] loaded (official4x skeleton)');
113
+ winston.info('[equipment-calendar] loaded (official4x CDN)');
142
114
  };
143
115
 
144
116
  plugin.addAdminMenu = async function (header) {
145
- header.plugins.push({
146
- route: '/plugins/equipment-calendar',
147
- icon: 'fa-calendar',
148
- name: 'Equipment Calendar',
149
- });
117
+ header.plugins.push({ route: '/plugins/equipment-calendar', icon: 'fa-calendar', name: 'Equipment Calendar' });
150
118
  return header;
151
119
  };
152
120
 
153
- // Optional: allow /calendar -> /equipment/calendar without hard redirect
154
- plugin.addRouteAliases = async function (hookData) {
155
- // hookData: { router, middleware, controllers } - but for filter:router.page we can inject page routes elsewhere;
156
- // Keep empty to avoid breaking installs. Alias handled by theme route config if needed.
157
- return hookData;
158
- };
159
-
160
121
  module.exports = plugin;
package/package.json CHANGED
@@ -1,24 +1,7 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "9.1.3",
4
- "description": "Equipment reservation calendar (NodeBB 4.x compatible, official settings pattern)",
3
+ "version": "9.1.5",
4
+ "description": "Equipment reservation calendar (NodeBB 4.x, official ACP settings pattern, FullCalendar via CDN)",
5
5
  "main": "library.js",
6
- "license": "MIT",
7
- "keywords": [
8
- "nodebb",
9
- "plugin",
10
- "calendar",
11
- "reservations",
12
- "fullcalendar"
13
- ],
14
- "dependencies": {
15
- "@fullcalendar/core": "^6.1.11",
16
- "@fullcalendar/daygrid": "^6.1.11",
17
- "@fullcalendar/interaction": "^6.1.11",
18
- "@fullcalendar/list": "^6.1.11",
19
- "@fullcalendar/timegrid": "^6.1.11"
20
- },
21
- "scripts": {
22
- "postinstall": "node ./scripts/postinstall.js"
23
- }
6
+ "license": "MIT"
24
7
  }
package/plugin.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "id": "nodebb-plugin-equipment-calendar",
3
3
  "name": "Equipment Calendar",
4
- "description": "Equipment reservation calendar with approvals and HelloAsso integration (skeleton).",
5
- "url": "https://github.com/your-org/nodebb-plugin-equipment-calendar",
4
+ "description": "Equipment reservation calendar (NodeBB 4.x, CDN assets)",
6
5
  "library": "./library.js",
7
6
  "hooks": [
8
7
  {
@@ -12,10 +11,6 @@
12
11
  {
13
12
  "hook": "filter:admin.header.build",
14
13
  "method": "addAdminMenu"
15
- },
16
- {
17
- "hook": "filter:router.page",
18
- "method": "addRouteAliases"
19
14
  }
20
15
  ],
21
16
  "acpScripts": [
package/public/js/acp.js CHANGED
@@ -1,40 +1,48 @@
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
- if (!url.startsWith('admin/plugins/equipment-calendar')) {
9
- return;
10
- }
7
+ if (!url.startsWith('admin/plugins/equipment-calendar')) return;
8
+
11
9
  const container = $('.equipment-calendar-settings');
12
10
  if (!container.length) return;
13
11
 
14
- // NodeBB 4.x settings module supports sync/persist (official pattern)
15
- if (typeof Settings.sync === 'function') {
16
- Settings.sync('equipmentCalendar', container);
17
- } else {
18
- Settings.load('equipmentCalendar', container);
19
- }
12
+ // load
13
+ try {
14
+ if (typeof Settings.sync === 'function') {
15
+ Settings.sync('equipmentCalendar', container);
16
+ } else {
17
+ Settings.load('equipmentCalendar', container);
18
+ }
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]]');
25
25
  };
26
- if (typeof Settings.persist === 'function') {
27
- Settings.persist('equipmentCalendar', container, done, true);
28
- } else {
29
- Settings.save('equipmentCalendar', container, done);
26
+ try {
27
+ if (typeof Settings.persist === 'function') {
28
+ Settings.persist('equipmentCalendar', container, done, true);
29
+ } else {
30
+ Settings.save('equipmentCalendar', container, done);
31
+ }
32
+ } catch (err) {
33
+ alerts.error(err && err.message ? err.message : 'Erreur');
30
34
  }
31
35
  });
32
36
  }
33
37
 
34
- Admin.init = function () {
35
- $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', onPage);
36
- onPage();
37
- };
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
+ });
38
43
 
39
- return Admin;
40
- });
44
+ // run once
45
+ require(['settings', 'alerts'], function (Settings, alerts) {
46
+ onEnd(Settings, alerts);
47
+ });
48
+ })();
@@ -1,5 +1,5 @@
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
 
@@ -8,9 +8,7 @@ define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
8
8
  return url === 'equipment/calendar' || url === 'calendar';
9
9
  }
10
10
 
11
- function isoDate(d) {
12
- return String(d || '').slice(0, 10);
13
- }
11
+ function isoDate(s) { return String(s || '').slice(0, 10); }
14
12
 
15
13
  async function loadEvents() {
16
14
  const res = await api.get('/plugins/equipment-calendar/events', {});
@@ -21,31 +19,33 @@ define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
21
19
  $('#ec-start').val(startIso);
22
20
  $('#ec-end').val(endIso);
23
21
  const modalEl = document.getElementById('ecModal');
24
- const modal = window.bootstrap ? window.bootstrap.Modal.getOrCreateInstance(modalEl) : null;
25
- if (modal) modal.show();
22
+ if (!modalEl || !window.bootstrap) return;
23
+ window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
26
24
  }
27
25
 
28
26
  async function createReservation(payload) {
29
- const res = await api.post('/plugins/equipment-calendar/reservations', payload);
30
- return res;
27
+ return api.post('/plugins/equipment-calendar/reservations', payload);
31
28
  }
32
29
 
33
30
  async function initCalendar() {
34
31
  const el = document.getElementById('ec-calendar');
35
- if (!el || !window.FullCalendar) return;
32
+ if (!el || !window.FullCalendar || !window.FullCalendar.Calendar) {
33
+ // show a useful error
34
+ if (el) {
35
+ el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN)</div>';
36
+ }
37
+ return;
38
+ }
36
39
 
37
40
  const events = await loadEvents();
38
41
 
39
- const calendar = new FullCalendar.Calendar(el, {
40
- plugins: [ FullCalendarDayGrid, FullCalendarTimeGrid, FullCalendarList, FullCalendarInteraction ],
42
+ const calendar = new window.FullCalendar.Calendar(el, {
41
43
  initialView: 'dayGridMonth',
42
44
  headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,listWeek' },
43
45
  selectable: true,
44
46
  selectMirror: true,
45
- eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
46
- allDaySlot: true,
47
+ displayEventTime: false,
47
48
  select: function (info) {
48
- // all-day selection end is exclusive -> store inclusive end
49
49
  const start = isoDate(info.startStr);
50
50
  const endExcl = isoDate(info.endStr);
51
51
  let end = endExcl;
@@ -61,7 +61,7 @@ define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
61
61
  const d = isoDate(info.dateStr);
62
62
  openModal(d, d);
63
63
  },
64
- events: events,
64
+ events,
65
65
  });
66
66
 
67
67
  calendar.render();
@@ -77,8 +77,7 @@ define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
77
77
  await createReservation({ startIso, endIso, itemIds, _csrf: csrf });
78
78
  alerts.success('Demande envoyée');
79
79
  const modalEl = document.getElementById('ecModal');
80
- const modal = window.bootstrap ? window.bootstrap.Modal.getOrCreateInstance(modalEl) : null;
81
- if (modal) modal.hide();
80
+ if (modalEl && window.bootstrap) window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
82
81
 
83
82
  const newEvents = await loadEvents();
84
83
  calendar.removeAllEvents();
@@ -89,10 +88,7 @@ define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
89
88
  });
90
89
  }
91
90
 
92
- function onEnd() {
93
- if (!pageMatch()) return;
94
- initCalendar();
95
- }
91
+ function onEnd() { if (pageMatch()) initCalendar(); }
96
92
 
97
93
  Client.init = function () {
98
94
  $(window).off('action:ajaxify.end.ecCal').on('action:ajaxify.end.ecCal', onEnd);
@@ -52,7 +52,7 @@
52
52
  </div>
53
53
 
54
54
  <div class="card card-body mb-3">
55
- <h5 class="mb-3">Réservation</h5>
55
+ <h5 class="mb-3">Calendrier</h5>
56
56
  <div class="row g-3">
57
57
  <div class="col-md-6">
58
58
  <label class="form-label">Timeout paiement (minutes)</label>
@@ -69,11 +69,5 @@
69
69
  </div>
70
70
  </div>
71
71
 
72
- <button id="save" class="btn btn-primary" type="button">Enregistrer</button>
72
+ <button id="ec-save" class="btn btn-primary" type="button">Enregistrer</button>
73
73
  </div>
74
-
75
- <script>
76
- require(['admin/plugins/equipment-calendar'], function (mod) {
77
- mod.init();
78
- });
79
- </script>
@@ -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,16 +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/core.index.global.min.css">
42
- <link rel="stylesheet" href="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/daygrid.index.global.min.css">
43
- <link rel="stylesheet" href="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/timegrid.index.global.min.css">
44
- <link rel="stylesheet" href="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/list.index.global.min.css">
45
-
46
- <script src="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/core.index.global.min.js"></script>
47
- <script src="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/interaction.index.global.min.js"></script>
48
- <script src="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/daygrid.index.global.min.js"></script>
49
- <script src="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/timegrid.index.global.min.js"></script>
50
- <script src="{config.relative_path}/plugins/nodebb-plugin-equipment-calendar/vendor/fullcalendar/list.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>
51
40
 
52
41
  <script>
53
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,29 +0,0 @@
1
-
2
- 'use strict';
3
- /* globals $, ajaxify */
4
- define('admin/plugins/equipment-calendar', ['settings', 'alerts'], function (Settings, alerts) {
5
- const Admin = {};
6
-
7
- function initOnPage() {
8
- 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
- }
12
-
13
- Settings.load('equipmentCalendar', $('.acp-page-container'));
14
-
15
- $('#save').off('click.ec').on('click.ec', function (e) {
16
- try { e.preventDefault(); } catch (err) {}
17
- Settings.save('equipmentCalendar', $('.acp-page-container'), function () {
18
- alerts.success('[[admin/settings:settings-saved]]');
19
- });
20
- });
21
- }
22
-
23
- Admin.init = function () {
24
- $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', initOnPage);
25
- initOnPage();
26
- };
27
-
28
- return Admin;
29
- });
@@ -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,37 +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 candidates = [
18
- // FullCalendar v6 provides "index.global.min.js" per package
19
- { pkg: '@fullcalendar/core', js: 'index.global.min.js', css: 'index.global.min.css' },
20
- { pkg: '@fullcalendar/daygrid', js: 'index.global.min.js', css: 'index.global.min.css' },
21
- { pkg: '@fullcalendar/timegrid', js: 'index.global.min.js', css: 'index.global.min.css' },
22
- { pkg: '@fullcalendar/list', js: 'index.global.min.js', css: 'index.global.min.css' },
23
- { pkg: '@fullcalendar/interaction', js: 'index.global.min.js', css: 'index.global.min.css' },
24
- ];
25
-
26
- const copied = [];
27
- candidates.forEach((c) => {
28
- const base = path.join(root, 'node_modules', c.pkg);
29
- const js = path.join(base, c.js);
30
- const css = path.join(base, c.css);
31
- if (exists(js)) { copyFile(js, path.join(outDir, `${c.pkg.replace('@fullcalendar/','')}.${c.js}`)); copied.push(js); }
32
- if (exists(css)) { copyFile(css, path.join(outDir, `${c.pkg.replace('@fullcalendar/','')}.${c.css}`)); copied.push(css); }
33
- });
34
-
35
- // Also write a small manifest for debugging
36
- fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify({ copied }, null, 2), 'utf-8');
37
- })();
@@ -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>