nodebb-plugin-equipment-calendar 9.1.6 → 9.1.8

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,9 +25,19 @@ 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
 
32
+ async function saveSettingsFromBody(body) {
33
+ const current = await getSettings();
34
+ const next = Object.assign({}, current, body || {});
35
+ next.paymentTimeoutMinutes = parseInt(next.paymentTimeoutMinutes, 10) || DEFAULT_SETTINGS.paymentTimeoutMinutes;
36
+ next.defaultView = next.defaultView || DEFAULT_SETTINGS.defaultView;
37
+ await meta.settings.set(SETTINGS_KEY, next);
38
+ return next;
39
+ }
40
+
33
41
  async function isUserInAnyGroups(uid, groupCsv) {
34
42
  if (!uid || !groupCsv) return false;
35
43
  const names = String(groupCsv).split(',').map(s => s.trim()).filter(Boolean);
@@ -66,39 +74,46 @@ function toEvent(r) {
66
74
  const end = new Date(String(r.endIso).slice(0,10) + 'T00:00:00Z');
67
75
  end.setUTCDate(end.getUTCDate() + 1);
68
76
  const endExcl = end.toISOString().slice(0,10);
69
-
70
77
  const status = r.status || 'pending';
71
78
  const title = status === 'paid' ? '[PAID] Reservation' : (status === 'approved' ? '[APPROVED] Reservation' : '[PENDING] Reservation');
72
79
  return { id: r.rid, title, start, end: endExcl, allDay: true };
73
80
  }
74
81
 
82
+ const plugin = {};
83
+
75
84
  plugin.init = async function (params) {
76
85
  const { router, middleware } = params;
77
86
 
78
- // ACP
79
87
  router.get('/admin/plugins/equipment-calendar', middleware.admin.buildHeader, async (req, res) => {
80
- res.render('admin/plugins/equipment-calendar', {});
88
+ const settings = await getSettings();
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
+ });
96
+ });
97
+
98
+ router.post('/admin/plugins/equipment-calendar', middleware.admin.checkPrivileges, middleware.applyCSRF, async (req, res) => {
99
+ await saveSettingsFromBody(req.body || {});
100
+ res.redirect(`${req.baseUrl}${req.path}?saved=1`);
81
101
  });
82
- router.get('/api/admin/plugins/equipment-calendar', async (req, res) => res.json({}));
83
102
 
84
- // Calendar page + alias /calendar
85
- async function renderCal(req, res) {
103
+ router.get('/equipment/calendar', middleware.buildHeader, async (req, res) => {
86
104
  const settings = await getSettings();
87
105
  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
-
92
- router.get('/api/equipment/calendar', async (req, res) => res.json({}));
93
- router.get('/api/calendar', async (req, res) => res.json({}));
106
+ });
107
+ router.get('/calendar', middleware.buildHeader, async (req, res) => {
108
+ const settings = await getSettings();
109
+ res.render('equipment-calendar/calendar', { defaultView: settings.defaultView });
110
+ });
94
111
 
95
- // Events
96
112
  router.get('/api/plugins/equipment-calendar/events', async (req, res) => {
97
113
  const list = await listReservations(1000);
98
114
  res.json({ events: list.map(toEvent) });
99
115
  });
100
116
 
101
- // Create reservation
102
117
  router.post('/api/plugins/equipment-calendar/reservations', middleware.applyCSRF, async (req, res) => {
103
118
  const uid = req.uid;
104
119
  if (!uid) return res.status(403).json({ message: 'forbidden' });
@@ -117,8 +132,6 @@ plugin.init = async function (params) {
117
132
  await saveReservation(r);
118
133
  res.json({ ok: true, rid: r.rid });
119
134
  });
120
-
121
- winston.info('[equipment-calendar] loaded (official4x CDN fix)');
122
135
  };
123
136
 
124
137
  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.6",
3
+ "version": "9.1.8",
4
4
  "main": "library.js",
5
5
  "license": "MIT"
6
6
  }
package/plugin.json CHANGED
@@ -12,9 +12,6 @@
12
12
  "method": "addAdminMenu"
13
13
  }
14
14
  ],
15
- "acpScripts": [
16
- "public/js/admin.js"
17
- ],
18
15
  "staticDirs": {
19
16
  "public": "public"
20
17
  }
@@ -1,79 +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" data-field="creatorGroups">
9
- </div>
10
- <div class="mb-3">
11
- <label class="form-label">Groupe valideur (approverGroup)</label>
12
- <input class="form-control" type="text" data-field="approverGroup">
13
- </div>
14
- <div class="mb-0">
15
- <label class="form-label">Groupe notifié (notifyGroup)</label>
16
- <input class="form-control" type="text" data-field="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" data-field="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" data-field="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" data-field="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" data-field="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" data-field="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" data-field="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" data-field="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" data-field="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" data-field="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="save" class="btn btn-primary" type="button">Enregistrer</button>
75
+ <button class="btn btn-primary" type="submit">Enregistrer</button>
76
+ </form>
73
77
  </div>
74
78
 
75
79
  <script>
76
- require(['admin/plugins/equipment-calendar'], function (mod) {
77
- mod.init();
78
- });
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
+ })();
79
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">
@@ -39,5 +40,102 @@
39
40
  <script src="https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js"></script>
40
41
 
41
42
  <script>
42
- require(['equipment-calendar/client'], function (mod) { mod.init(); });
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
+ })();
43
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,38 +0,0 @@
1
- 'use strict';
2
- /* globals $, ajaxify */
3
- define('admin/plugins/equipment-calendar', ['settings', 'alerts'], function (Settings, alerts) {
4
- const Admin = {};
5
-
6
- function onPage() {
7
- const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : (window.location.pathname || '');
8
- if (!url.startsWith('admin/plugins/equipment-calendar')) return;
9
-
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
- }
18
-
19
- $('#save').off('click.ec').on('click.ec', function (e) {
20
- e.preventDefault();
21
- const done = function () {
22
- alerts.success('[[admin/settings:settings-saved]]');
23
- };
24
- if (typeof Settings.persist === 'function') {
25
- Settings.persist('equipmentCalendar', container, done, true);
26
- } else {
27
- Settings.save('equipmentCalendar', container, done);
28
- }
29
- });
30
- }
31
-
32
- Admin.init = function () {
33
- $(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', onPage);
34
- onPage();
35
- };
36
-
37
- return Admin;
38
- });
@@ -1,98 +0,0 @@
1
- 'use strict';
2
- /* globals $, ajaxify */
3
- define('equipment-calendar/client', ['api', 'alerts'], function (api, alerts) {
4
- const Client = {};
5
-
6
- function pageMatch() {
7
- const url = (ajaxify && ajaxify.data && ajaxify.data.url) ? ajaxify.data.url : '';
8
- return url === 'equipment/calendar' || url === 'calendar';
9
- }
10
-
11
- function isoDate(s) { return String(s || '').slice(0, 10); }
12
-
13
- async function loadEvents() {
14
- const res = await api.get('/plugins/equipment-calendar/events', {});
15
- return (res && res.events) ? res.events : [];
16
- }
17
-
18
- function openModal(startIso, endIso) {
19
- $('#ec-start').val(startIso);
20
- $('#ec-end').val(endIso);
21
- const modalEl = document.getElementById('ecModal');
22
- if (!modalEl || !window.bootstrap) return;
23
- window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
24
- }
25
-
26
- async function createReservation(payload) {
27
- return api.post('/plugins/equipment-calendar/reservations', payload);
28
- }
29
-
30
- async function initCalendar() {
31
- const el = document.getElementById('ec-calendar');
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
- }
38
-
39
- const events = await loadEvents();
40
-
41
- const calendar = new window.FullCalendar.Calendar(el, {
42
- initialView: 'dayGridMonth',
43
- headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,listWeek' },
44
- selectable: true,
45
- selectMirror: true,
46
- displayEventTime: false,
47
- select: function (info) {
48
- const start = isoDate(info.startStr);
49
- const endExcl = isoDate(info.endStr);
50
- let end = endExcl;
51
- if (endExcl) {
52
- const d = new Date(endExcl + 'T00:00:00Z');
53
- d.setUTCDate(d.getUTCDate() - 1);
54
- end = d.toISOString().slice(0, 10);
55
- }
56
- openModal(start, end);
57
- calendar.unselect();
58
- },
59
- dateClick: function (info) {
60
- const d = isoDate(info.dateStr);
61
- openModal(d, d);
62
- },
63
- events,
64
- });
65
-
66
- calendar.render();
67
-
68
- $('#ec-form').off('submit.ec').on('submit.ec', async function (e) {
69
- e.preventDefault();
70
- const startIso = $('#ec-start').val();
71
- const endIso = $('#ec-end').val();
72
- const itemIds = String($('#ec-items').val() || '').split(',').map(s => s.trim()).filter(Boolean);
73
- const csrf = $(this).find('input[name="_csrf"]').val();
74
-
75
- try {
76
- await createReservation({ startIso, endIso, itemIds, _csrf: csrf });
77
- alerts.success('Demande envoyée');
78
- const modalEl = document.getElementById('ecModal');
79
- if (modalEl && window.bootstrap) window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
80
-
81
- const newEvents = await loadEvents();
82
- calendar.removeAllEvents();
83
- newEvents.forEach(ev => calendar.addEvent(ev));
84
- } catch (err) {
85
- alerts.error(err && err.message ? err.message : 'Erreur');
86
- }
87
- });
88
- }
89
-
90
- function onEnd() { if (pageMatch()) initCalendar(); }
91
-
92
- Client.init = function () {
93
- $(window).off('action:ajaxify.end.ecCal').on('action:ajaxify.end.ecCal', onEnd);
94
- onEnd();
95
- };
96
-
97
- return Client;
98
- });