nodebb-plugin-equipment-calendar 9.1.7 → 9.1.9

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
@@ -3,9 +3,7 @@
3
3
  const meta = require.main.require('./src/meta');
4
4
  const db = require.main.require('./src/database');
5
5
  const groups = require.main.require('./src/groups');
6
- const winston = require.main.require('winston');
7
6
 
8
- const plugin = {};
9
7
  const SETTINGS_KEY = 'equipmentCalendar';
10
8
 
11
9
  const DEFAULT_SETTINGS = {
@@ -27,14 +25,15 @@ async function getSettings() {
27
25
  const raw = await meta.settings.get(SETTINGS_KEY);
28
26
  const s = Object.assign({}, DEFAULT_SETTINGS, raw || {});
29
27
  s.paymentTimeoutMinutes = parseInt(s.paymentTimeoutMinutes, 10) || DEFAULT_SETTINGS.paymentTimeoutMinutes;
28
+ s.defaultView = s.defaultView || DEFAULT_SETTINGS.defaultView;
30
29
  return s;
31
30
  }
32
31
 
33
- async function setSettings(payload) {
32
+ async function saveSettingsFromBody(body) {
34
33
  const current = await getSettings();
35
- const next = Object.assign({}, current, payload || {});
36
- // Normalize
34
+ const next = Object.assign({}, current, body || {});
37
35
  next.paymentTimeoutMinutes = parseInt(next.paymentTimeoutMinutes, 10) || DEFAULT_SETTINGS.paymentTimeoutMinutes;
36
+ next.defaultView = next.defaultView || DEFAULT_SETTINGS.defaultView;
38
37
  await meta.settings.set(SETTINGS_KEY, next);
39
38
  return next;
40
39
  }
@@ -75,46 +74,53 @@ function toEvent(r) {
75
74
  const end = new Date(String(r.endIso).slice(0,10) + 'T00:00:00Z');
76
75
  end.setUTCDate(end.getUTCDate() + 1);
77
76
  const endExcl = end.toISOString().slice(0,10);
78
-
79
77
  const status = r.status || 'pending';
80
78
  const title = status === 'paid' ? '[PAID] Reservation' : (status === 'approved' ? '[APPROVED] Reservation' : '[PENDING] Reservation');
81
79
  return { id: r.rid, title, start, end: endExcl, allDay: true };
82
80
  }
83
81
 
82
+ const plugin = {};
83
+
84
84
  plugin.init = async function (params) {
85
85
  const { router, middleware } = params;
86
86
 
87
- // ACP page
88
87
  router.get('/admin/plugins/equipment-calendar', middleware.admin.buildHeader, async (req, res) => {
89
- res.render('admin/plugins/equipment-calendar', {});
90
- });
91
- router.get('/api/admin/plugins/equipment-calendar', async (req, res) => res.json({}));
92
-
93
- // ACP settings API (official-style behaviour without requirejs)
94
- router.get('/api/admin/plugins/equipment-calendar/settings', middleware.admin.checkPrivileges, async (req, res) => {
95
88
  const settings = await getSettings();
96
- res.json({ settings });
89
+ res.render('admin/plugins/equipment-calendar', {
90
+ settings,
91
+ savedFlag: (req.query && req.query.saved === '1') ? 'true' : 'false',
92
+ selected_dayGridMonth: settings.defaultView === 'dayGridMonth' ? 'selected' : '',
93
+ selected_timeGridWeek: settings.defaultView === 'timeGridWeek' ? 'selected' : '',
94
+ selected_listWeek: settings.defaultView === 'listWeek' ? 'selected' : '',
95
+ });
97
96
  });
98
- router.post('/api/admin/plugins/equipment-calendar/settings', middleware.admin.checkPrivileges, middleware.applyCSRF, async (req, res) => {
99
- const saved = await setSettings(req.body || {});
100
- res.json({ ok: true, settings: saved });
97
+ // NodeBB ACP ajaxify expects this route for plugin pages
98
+ router.get('/api/admin/plugins/equipment-calendar', middleware.admin.checkPrivileges, async (req, res) => {
99
+ const settings = await getSettings();
100
+ res.json({ settings });
101
+ });
102
+
103
+
104
+
105
+ router.post('/admin/plugins/equipment-calendar', middleware.admin.checkPrivileges, middleware.applyCSRF, async (req, res) => {
106
+ await saveSettingsFromBody(req.body || {});
107
+ res.redirect(`${req.baseUrl}${req.path}?saved=1`);
101
108
  });
102
109
 
103
- // Calendar page + /calendar alias
104
- async function renderCal(req, res) {
110
+ router.get('/equipment/calendar', middleware.buildHeader, async (req, res) => {
105
111
  const settings = await getSettings();
106
112
  res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
107
- }
108
- router.get('/equipment/calendar', middleware.buildHeader, renderCal);
109
- router.get('/calendar', middleware.buildHeader, renderCal);
113
+ });
114
+ router.get('/calendar', middleware.buildHeader, async (req, res) => {
115
+ const settings = await getSettings();
116
+ res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
117
+ });
110
118
 
111
- // Events
112
119
  router.get('/api/plugins/equipment-calendar/events', async (req, res) => {
113
120
  const list = await listReservations(1000);
114
121
  res.json({ events: list.map(toEvent) });
115
122
  });
116
123
 
117
- // Create reservation
118
124
  router.post('/api/plugins/equipment-calendar/reservations', middleware.applyCSRF, async (req, res) => {
119
125
  const uid = req.uid;
120
126
  if (!uid) return res.status(403).json({ message: 'forbidden' });
@@ -133,8 +139,6 @@ plugin.init = async function (params) {
133
139
  await saveReservation(r);
134
140
  res.json({ ok: true, rid: r.rid });
135
141
  });
136
-
137
- winston.info('[equipment-calendar] loaded (NodeBB 4.x no-require)');
138
142
  };
139
143
 
140
144
  plugin.addAdminMenu = async function (header) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-equipment-calendar",
3
- "version": "9.1.7",
3
+ "version": "9.1.9",
4
4
  "main": "library.js",
5
5
  "license": "MIT"
6
6
  }
package/plugin.json CHANGED
@@ -12,12 +12,6 @@
12
12
  "method": "addAdminMenu"
13
13
  }
14
14
  ],
15
- "scripts": [
16
- "public/js/client.js"
17
- ],
18
- "acpScripts": [
19
- "public/js/admin.js"
20
- ],
21
15
  "staticDirs": {
22
16
  "public": "public"
23
17
  }
@@ -1,73 +1,86 @@
1
- <div class="acp-page-container equipment-calendar-settings">
1
+ <div class="acp-page-container">
2
2
  <h1 class="mb-3">Equipment Calendar</h1>
3
3
 
4
- <div class="card card-body mb-3">
5
- <h5 class="mb-3">Permissions</h5>
6
- <div class="mb-3">
7
- <label class="form-label">Groupes autorisés à créer (creatorGroups, séparés par virgule)</label>
8
- <input class="form-control" type="text" id="creatorGroups">
9
- </div>
10
- <div class="mb-3">
11
- <label class="form-label">Groupe valideur (approverGroup)</label>
12
- <input class="form-control" type="text" id="approverGroup">
13
- </div>
14
- <div class="mb-0">
15
- <label class="form-label">Groupe notifié (notifyGroup)</label>
16
- <input class="form-control" type="text" id="notifyGroup">
17
- </div>
18
- </div>
4
+ <form method="post" action="{config.relative_path}/admin/plugins/equipment-calendar">
5
+ <input type="hidden" name="_csrf" value="{csrf_token}">
19
6
 
20
- <div class="card card-body mb-3">
21
- <h5 class="mb-3">HelloAsso</h5>
22
- <div class="row g-3">
23
- <div class="col-md-6">
24
- <label class="form-label">API Base URL (prod/sandbox)</label>
25
- <input class="form-control" type="text" id="ha_apiBaseUrl" placeholder="https://api.helloasso.com ou https://api.helloasso-sandbox.com">
26
- </div>
27
- <div class="col-md-6">
28
- <label class="form-label">Organization slug</label>
29
- <input class="form-control" type="text" id="ha_organizationSlug">
30
- </div>
31
- <div class="col-md-6">
32
- <label class="form-label">Client ID</label>
33
- <input class="form-control" type="text" id="ha_clientId">
7
+ <div class="card card-body mb-3">
8
+ <h5 class="mb-3">Permissions</h5>
9
+ <div class="mb-3">
10
+ <label class="form-label">Groupes autorisés à créer (creatorGroups, séparés par virgule)</label>
11
+ <input class="form-control" type="text" name="creatorGroups" value="{settings.creatorGroups}">
34
12
  </div>
35
- <div class="col-md-6">
36
- <label class="form-label">Client Secret</label>
37
- <input class="form-control" type="password" id="ha_clientSecret">
13
+ <div class="mb-3">
14
+ <label class="form-label">Groupe valideur (approverGroup)</label>
15
+ <input class="form-control" type="text" name="approverGroup" value="{settings.approverGroup}">
38
16
  </div>
39
- <div class="col-md-6">
40
- <label class="form-label">Form Type</label>
41
- <input class="form-control" type="text" id="ha_itemsFormType" placeholder="shop">
42
- </div>
43
- <div class="col-md-6">
44
- <label class="form-label">Form Slug</label>
45
- <input class="form-control" type="text" id="ha_itemsFormSlug" placeholder="locations-materiel-2026">
46
- </div>
47
- <div class="col-12">
48
- <label class="form-label">Return URL base (callback)</label>
49
- <input class="form-control" type="text" id="ha_returnUrl" placeholder="https://www.onekite.com">
17
+ <div class="mb-0">
18
+ <label class="form-label">Groupe notifié (notifyGroup)</label>
19
+ <input class="form-control" type="text" name="notifyGroup" value="{settings.notifyGroup}">
50
20
  </div>
51
21
  </div>
52
- </div>
53
22
 
54
- <div class="card card-body mb-3">
55
- <h5 class="mb-3">Calendrier</h5>
56
- <div class="row g-3">
57
- <div class="col-md-6">
58
- <label class="form-label">Timeout paiement (minutes)</label>
59
- <input class="form-control" type="number" min="1" id="paymentTimeoutMinutes">
23
+ <div class="card card-body mb-3">
24
+ <h5 class="mb-3">HelloAsso</h5>
25
+ <div class="row g-3">
26
+ <div class="col-md-6">
27
+ <label class="form-label">API Base URL (prod/sandbox)</label>
28
+ <input class="form-control" type="text" name="ha_apiBaseUrl" value="{settings.ha_apiBaseUrl}" placeholder="https://api.helloasso.com ou https://api.helloasso-sandbox.com">
29
+ </div>
30
+ <div class="col-md-6">
31
+ <label class="form-label">Organization slug</label>
32
+ <input class="form-control" type="text" name="ha_organizationSlug" value="{settings.ha_organizationSlug}">
33
+ </div>
34
+ <div class="col-md-6">
35
+ <label class="form-label">Client ID</label>
36
+ <input class="form-control" type="text" name="ha_clientId" value="{settings.ha_clientId}">
37
+ </div>
38
+ <div class="col-md-6">
39
+ <label class="form-label">Client Secret</label>
40
+ <input class="form-control" type="password" name="ha_clientSecret" value="{settings.ha_clientSecret}">
41
+ </div>
42
+ <div class="col-md-6">
43
+ <label class="form-label">Form Type</label>
44
+ <input class="form-control" type="text" name="ha_itemsFormType" value="{settings.ha_itemsFormType}" placeholder="shop">
45
+ </div>
46
+ <div class="col-md-6">
47
+ <label class="form-label">Form Slug</label>
48
+ <input class="form-control" type="text" name="ha_itemsFormSlug" value="{settings.ha_itemsFormSlug}" placeholder="locations-materiel-2026">
49
+ </div>
50
+ <div class="col-12">
51
+ <label class="form-label">Return URL base (callback)</label>
52
+ <input class="form-control" type="text" name="ha_returnUrl" value="{settings.ha_returnUrl}" placeholder="https://www.onekite.com">
53
+ </div>
60
54
  </div>
61
- <div class="col-md-6">
62
- <label class="form-label">Vue par défaut</label>
63
- <select class="form-select" id="defaultView">
64
- <option value="dayGridMonth">Mois</option>
65
- <option value="timeGridWeek">Semaine</option>
66
- <option value="listWeek">Liste</option>
67
- </select>
55
+ </div>
56
+
57
+ <div class="card card-body mb-3">
58
+ <h5 class="mb-3">Calendrier</h5>
59
+ <div class="row g-3">
60
+ <div class="col-md-6">
61
+ <label class="form-label">Timeout paiement (minutes)</label>
62
+ <input class="form-control" type="number" min="1" name="paymentTimeoutMinutes" value="{settings.paymentTimeoutMinutes}">
63
+ </div>
64
+ <div class="col-md-6">
65
+ <label class="form-label">Vue par défaut</label>
66
+ <select class="form-select" name="defaultView">
67
+ <option value="dayGridMonth" {selected_dayGridMonth}>Mois</option>
68
+ <option value="timeGridWeek" {selected_timeGridWeek}>Semaine</option>
69
+ <option value="listWeek" {selected_listWeek}>Liste</option>
70
+ </select>
71
+ </div>
68
72
  </div>
69
73
  </div>
70
- </div>
71
74
 
72
- <button id="ec-save" class="btn btn-primary" type="button">Enregistrer</button>
75
+ <button class="btn btn-primary" type="submit">Enregistrer</button>
76
+ </form>
73
77
  </div>
78
+
79
+ <script>
80
+ (function () {
81
+ var saved = {savedFlag};
82
+ if (saved && window.app && typeof window.app.alertSuccess === 'function') {
83
+ window.app.alertSuccess('[[admin/settings:settings-saved]]');
84
+ }
85
+ })();
86
+ </script>
@@ -1,5 +1,6 @@
1
1
  <div class="container-fluid px-3 equipment-calendar-page">
2
2
  <h1 class="mb-3">Calendrier des réservations</h1>
3
+
3
4
  <div id="ec-calendar"></div>
4
5
 
5
6
  <div class="modal fade" id="ecModal" tabindex="-1" aria-hidden="true">
@@ -14,15 +15,15 @@
14
15
  <input type="hidden" name="_csrf" value="{csrf_token}">
15
16
  <div class="mb-3">
16
17
  <label class="form-label">Du</label>
17
- <input class="form-control" type="date" name="startIso" id="ec-start" required>
18
+ <input class="form-control" type="date" id="ec-start" required>
18
19
  </div>
19
20
  <div class="mb-3">
20
21
  <label class="form-label">Au</label>
21
- <input class="form-control" type="date" name="endIso" id="ec-end" required>
22
+ <input class="form-control" type="date" id="ec-end" required>
22
23
  </div>
23
24
  <div class="mb-3">
24
25
  <label class="form-label">Matériel (IDs, séparés par virgule)</label>
25
- <input class="form-control" type="text" name="itemIds" id="ec-items" placeholder="ex: item123,item456" required>
26
+ <input class="form-control" type="text" id="ec-items" placeholder="ex: item123,item456" required>
26
27
  </div>
27
28
  </div>
28
29
  <div class="modal-footer">
@@ -37,3 +38,104 @@
37
38
 
38
39
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.css">
39
40
  <script src="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js"></script>
41
+
42
+ <script>
43
+ (function () {
44
+ function isoDate(s) { return String(s || '').slice(0, 10); }
45
+ function alertSuccess(msg){ if (window.app && app.alertSuccess) app.alertSuccess(msg); }
46
+ function alertError(msg){ if (window.app && app.alertError) app.alertError(msg); else console.error(msg); }
47
+
48
+ async function apiGet(path) {
49
+ var res = await fetch(config.relative_path + path, { credentials: 'same-origin' });
50
+ if (!res.ok) throw new Error('API error');
51
+ return res.json();
52
+ }
53
+ async function apiPost(path, body, csrf) {
54
+ var res = await fetch(config.relative_path + path, {
55
+ method: 'POST',
56
+ credentials: 'same-origin',
57
+ headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrf || '' },
58
+ body: JSON.stringify(body || {}),
59
+ });
60
+ var data = {};
61
+ try { data = await res.json(); } catch (e) {}
62
+ if (!res.ok) throw new Error(data.message || 'Erreur');
63
+ return data;
64
+ }
65
+
66
+ function openModal(startIso, endIso) {
67
+ document.getElementById('ec-start').value = startIso;
68
+ document.getElementById('ec-end').value = endIso;
69
+ var modalEl = document.getElementById('ecModal');
70
+ if (window.bootstrap && modalEl) {
71
+ window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
72
+ }
73
+ }
74
+
75
+ async function init() {
76
+ var el = document.getElementById('ec-calendar');
77
+ if (!el) return;
78
+
79
+ if (!window.FullCalendar || !window.FullCalendar.Calendar) {
80
+ el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN). Vérifie la CSP et l’accès à cdn.jsdelivr.net.</div>';
81
+ return;
82
+ }
83
+
84
+ var data = await apiGet('/api/plugins/equipment-calendar/events');
85
+ var events = (data && data.events) ? data.events : [];
86
+
87
+ var calendar = new window.FullCalendar.Calendar(el, {
88
+ initialView: '{defaultView}',
89
+ headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,listWeek' },
90
+ selectable: true,
91
+ selectMirror: true,
92
+ displayEventTime: false,
93
+ select: function (info) {
94
+ var start = isoDate(info.startStr);
95
+ var endExcl = isoDate(info.endStr);
96
+ var end = endExcl;
97
+ if (endExcl) {
98
+ var d = new Date(endExcl + 'T00:00:00Z');
99
+ d.setUTCDate(d.getUTCDate() - 1);
100
+ end = d.toISOString().slice(0, 10);
101
+ }
102
+ openModal(start, end);
103
+ calendar.unselect();
104
+ },
105
+ dateClick: function (info) {
106
+ var d = isoDate(info.dateStr);
107
+ openModal(d, d);
108
+ },
109
+ events: events,
110
+ });
111
+ calendar.render();
112
+
113
+ var form = document.getElementById('ec-form');
114
+ if (form && !form.__bound) {
115
+ form.__bound = true;
116
+ form.addEventListener('submit', async function (e) {
117
+ e.preventDefault();
118
+ var startIso = document.getElementById('ec-start').value;
119
+ var endIso = document.getElementById('ec-end').value;
120
+ var itemIds = String(document.getElementById('ec-items').value || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
121
+ var csrf = form.querySelector('input[name="_csrf"]').value;
122
+
123
+ try {
124
+ await apiPost('/api/plugins/equipment-calendar/reservations', { startIso: startIso, endIso: endIso, itemIds: itemIds }, csrf);
125
+ alertSuccess('Demande envoyée');
126
+ var modalEl = document.getElementById('ecModal');
127
+ if (window.bootstrap && modalEl) window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
128
+
129
+ var data2 = await apiGet('/api/plugins/equipment-calendar/events');
130
+ calendar.removeAllEvents();
131
+ (data2.events || []).forEach(function (ev) { calendar.addEvent(ev); });
132
+ } catch (err) {
133
+ alertError(err.message || 'Erreur');
134
+ }
135
+ });
136
+ }
137
+ }
138
+
139
+ init().catch(function () {});
140
+ })();
141
+ </script>
package/public/js/acp.js DELETED
@@ -1,48 +0,0 @@
1
- 'use strict';
2
- /* globals $, ajaxify, require */
3
-
4
- (function () {
5
- function onEnd(Settings, alerts) {
6
- const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : (window.location.pathname || '');
7
- if (!url.startsWith('admin/plugins/equipment-calendar')) return;
8
-
9
- const container = $('.equipment-calendar-settings');
10
- if (!container.length) return;
11
-
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
-
21
- $('#ec-save').off('click.ec').on('click.ec', function (e) {
22
- e.preventDefault();
23
- const done = function () {
24
- alerts.success('[[admin/settings:settings-saved]]');
25
- };
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');
34
- }
35
- });
36
- }
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
- });
43
-
44
- // run once
45
- require(['settings', 'alerts'], function (Settings, alerts) {
46
- onEnd(Settings, alerts);
47
- });
48
- })();
@@ -1,109 +0,0 @@
1
- 'use strict';
2
- /* globals ajaxify, app */
3
-
4
- (function () {
5
- function alertSuccess(msg) {
6
- if (window.app && typeof window.app.alertSuccess === 'function') return window.app.alertSuccess(msg);
7
- // fallback: simple
8
- console.log('[success]', msg);
9
- }
10
- function alertError(msg) {
11
- if (window.app && typeof window.app.alertError === 'function') return window.app.alertError(msg);
12
- console.error('[error]', msg);
13
- }
14
-
15
- function isAcpPage() {
16
- const path = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : '';
17
- return path.startsWith('admin/plugins/equipment-calendar');
18
- }
19
-
20
- function csrfToken() {
21
- // NodeBB exposes csrf_token in ajaxify.data when on ACP pages
22
- return (ajaxify && ajaxify.data && ajaxify.data.csrf_token) ? ajaxify.data.csrf_token : '';
23
- }
24
-
25
- function q(id) { return document.getElementById(id); }
26
-
27
- function readForm() {
28
- return {
29
- creatorGroups: q('creatorGroups')?.value || '',
30
- approverGroup: q('approverGroup')?.value || '',
31
- notifyGroup: q('notifyGroup')?.value || '',
32
- ha_apiBaseUrl: q('ha_apiBaseUrl')?.value || '',
33
- ha_organizationSlug: q('ha_organizationSlug')?.value || '',
34
- ha_clientId: q('ha_clientId')?.value || '',
35
- ha_clientSecret: q('ha_clientSecret')?.value || '',
36
- ha_itemsFormType: q('ha_itemsFormType')?.value || '',
37
- ha_itemsFormSlug: q('ha_itemsFormSlug')?.value || '',
38
- ha_returnUrl: q('ha_returnUrl')?.value || '',
39
- paymentTimeoutMinutes: q('paymentTimeoutMinutes')?.value || '',
40
- defaultView: q('defaultView')?.value || '',
41
- };
42
- }
43
-
44
- function fillForm(s) {
45
- if (!s) return;
46
- const set = (id, v) => { const el = q(id); if (el) el.value = v ?? ''; };
47
- set('creatorGroups', s.creatorGroups);
48
- set('approverGroup', s.approverGroup);
49
- set('notifyGroup', s.notifyGroup);
50
- set('ha_apiBaseUrl', s.ha_apiBaseUrl);
51
- set('ha_organizationSlug', s.ha_organizationSlug);
52
- set('ha_clientId', s.ha_clientId);
53
- set('ha_clientSecret', s.ha_clientSecret);
54
- set('ha_itemsFormType', s.ha_itemsFormType);
55
- set('ha_itemsFormSlug', s.ha_itemsFormSlug);
56
- set('ha_returnUrl', s.ha_returnUrl);
57
- set('paymentTimeoutMinutes', s.paymentTimeoutMinutes);
58
- set('defaultView', s.defaultView);
59
- }
60
-
61
- async function loadSettings() {
62
- const res = await fetch(`${config.relative_path}/api/admin/plugins/equipment-calendar/settings`, { credentials: 'same-origin' });
63
- const data = await res.json();
64
- fillForm(data.settings);
65
- }
66
-
67
- async function saveSettings() {
68
- const payload = readForm();
69
- const res = await fetch(`${config.relative_path}/api/admin/plugins/equipment-calendar/settings`, {
70
- method: 'POST',
71
- credentials: 'same-origin',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- 'x-csrf-token': csrfToken(),
75
- },
76
- body: JSON.stringify(payload),
77
- });
78
- const data = await res.json().catch(() => ({}));
79
- if (!res.ok) throw new Error(data.message || 'Save failed');
80
- alertSuccess('[[admin/settings:settings-saved]]');
81
- }
82
-
83
- async function init() {
84
- if (!isAcpPage()) return;
85
- if (!document.querySelector('.equipment-calendar-settings')) return;
86
-
87
- try { await loadSettings(); } catch (e) { /* ignore */ }
88
-
89
- const btn = q('ec-save');
90
- if (btn) {
91
- btn.addEventListener('click', async (e) => {
92
- e.preventDefault();
93
- try { await saveSettings(); } catch (err) { alertError(err.message || 'Erreur'); }
94
- });
95
- }
96
- }
97
-
98
- // Run on initial load
99
- if (document.readyState === 'loading') {
100
- document.addEventListener('DOMContentLoaded', init);
101
- } else {
102
- init();
103
- }
104
-
105
- // Also run after ajaxify navigation if available
106
- if (window.jQuery) {
107
- window.jQuery(window).on('action:ajaxify.end', init);
108
- }
109
- })();
@@ -1,130 +0,0 @@
1
- 'use strict';
2
- /* globals ajaxify */
3
-
4
- (function () {
5
- function pageMatch() {
6
- const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : '';
7
- return url === 'equipment/calendar' || url === 'calendar';
8
- }
9
-
10
- function isoDate(s) { return String(s || '').slice(0, 10); }
11
-
12
- async function apiGet(path) {
13
- const res = await fetch(`${config.relative_path}${path}`, { credentials: 'same-origin' });
14
- if (!res.ok) throw new Error('API error');
15
- return res.json();
16
- }
17
-
18
- async function apiPost(path, body, csrf) {
19
- const res = await fetch(`${config.relative_path}${path}`, {
20
- method: 'POST',
21
- credentials: 'same-origin',
22
- headers: {
23
- 'Content-Type': 'application/json',
24
- 'x-csrf-token': csrf || '',
25
- },
26
- body: JSON.stringify(body),
27
- });
28
- const data = await res.json().catch(() => ({}));
29
- if (!res.ok) throw new Error(data.message || 'Erreur');
30
- return data;
31
- }
32
-
33
- function csrfTokenFromForm() {
34
- const el = document.querySelector('#ec-form input[name="_csrf"]');
35
- return el ? el.value : '';
36
- }
37
-
38
- function openModal(startIso, endIso) {
39
- const start = document.getElementById('ec-start');
40
- const end = document.getElementById('ec-end');
41
- if (start) start.value = startIso;
42
- if (end) end.value = endIso;
43
-
44
- const modalEl = document.getElementById('ecModal');
45
- if (!modalEl || !window.bootstrap) return;
46
- window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
47
- }
48
-
49
- async function initCalendar() {
50
- if (!pageMatch()) return;
51
- const el = document.getElementById('ec-calendar');
52
- if (!el) return;
53
-
54
- if (!window.FullCalendar || !window.FullCalendar.Calendar) {
55
- el.innerHTML = '<div class="alert alert-danger">FullCalendar non chargé (CDN). Vérifie la CSP et l’accès à cdn.jsdelivr.net.</div>';
56
- return;
57
- }
58
-
59
- const data = await apiGet('/api/plugins/equipment-calendar/events');
60
- const events = (data && data.events) ? data.events : [];
61
-
62
- const calendar = new window.FullCalendar.Calendar(el, {
63
- initialView: 'dayGridMonth',
64
- headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,listWeek' },
65
- selectable: true,
66
- selectMirror: true,
67
- displayEventTime: false,
68
- select: function (info) {
69
- const start = isoDate(info.startStr);
70
- const endExcl = isoDate(info.endStr);
71
- let end = endExcl;
72
- if (endExcl) {
73
- const d = new Date(endExcl + 'T00:00:00Z');
74
- d.setUTCDate(d.getUTCDate() - 1);
75
- end = d.toISOString().slice(0, 10);
76
- }
77
- openModal(start, end);
78
- calendar.unselect();
79
- },
80
- dateClick: function (info) {
81
- const d = isoDate(info.dateStr);
82
- openModal(d, d);
83
- },
84
- events,
85
- });
86
- calendar.render();
87
-
88
- const form = document.getElementById('ec-form');
89
- if (form) {
90
- form.addEventListener('submit', async (e) => {
91
- e.preventDefault();
92
- const startIso = document.getElementById('ec-start')?.value || '';
93
- const endIso = document.getElementById('ec-end')?.value || '';
94
- const itemIds = String(document.getElementById('ec-items')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
95
- const csrf = csrfTokenFromForm();
96
-
97
- try {
98
- await apiPost('/api/plugins/equipment-calendar/reservations', { startIso, endIso, itemIds }, csrf);
99
-
100
- if (window.app && typeof window.app.alertSuccess === 'function') {
101
- window.app.alertSuccess('Demande envoyée');
102
- }
103
- const modalEl = document.getElementById('ecModal');
104
- if (modalEl && window.bootstrap) window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
105
-
106
- const data2 = await apiGet('/api/plugins/equipment-calendar/events');
107
- calendar.removeAllEvents();
108
- (data2.events || []).forEach(ev => calendar.addEvent(ev));
109
- } catch (err) {
110
- if (window.app && typeof window.app.alertError === 'function') {
111
- window.app.alertError(err.message || 'Erreur');
112
- } else {
113
- alert(err.message || 'Erreur');
114
- }
115
- }
116
- }, { once: true });
117
- }
118
- }
119
-
120
- function init() { initCalendar().catch(() => {}); }
121
-
122
- if (document.readyState === 'loading') {
123
- document.addEventListener('DOMContentLoaded', init);
124
- } else {
125
- init();
126
- }
127
- if (window.jQuery) {
128
- window.jQuery(window).on('action:ajaxify.end', init);
129
- }
130
- })();