nodebb-plugin-onekite-calendar 2.0.12 → 2.0.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog – calendar-onekite
2
2
 
3
+ ## 1.3.6
4
+ - ACP Comptabilisation : ajout d’un tableau séparé « Détails des sorties gratuites » (avec le nom du matériel).
5
+ - Mobile FAB : la modale utilise maintenant des champs date avec calendrier (type="date") et empêche toute réservation pour le jour même ou dans le passé.
6
+
3
7
  ## 1.3.5
4
8
  - ACP : ajout d’un paramètre « Location longue durée (jours) pour validateurs ». Si une réservation faite par un validateur dépasse ce nombre de jours, elle redevient payante et suit le workflow normal (demande → validation → paiement HelloAsso). Mets 0 pour conserver le comportement « toujours gratuit ».
5
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -1,5 +1,9 @@
1
1
  # Changelog – calendar-onekite
2
2
 
3
+ ## 1.3.6
4
+ - ACP Comptabilisation : ajout d’un tableau séparé « Détails des sorties gratuites » (avec le nom du matériel).
5
+ - Mobile FAB : la modale utilise maintenant des champs date avec calendrier (type="date") et empêche toute réservation pour le jour même ou dans le passé.
6
+
3
7
  ## 1.3.5
4
8
  - ACP : ajout d’un paramètre « Location longue durée (jours) pour validateurs ». Si une réservation faite par un validateur dépasse ce nombre de jours, elle redevient payante et suit le workflow normal (demande → validation → paiement HelloAsso). Mets 0 pour conserver le comportement « toujours gratuit ».
5
9
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "1.3.5"
42
+ "version": "1.3.6"
43
43
  }
@@ -1170,6 +1170,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
1170
1170
  const accPurge = document.getElementById('onekite-acc-purge');
1171
1171
  const accSummary = document.querySelector('#onekite-acc-summary tbody');
1172
1172
  const accRows = document.querySelector('#onekite-acc-rows tbody');
1173
+ const accFreeRows = document.querySelector('#onekite-acc-free-rows tbody');
1173
1174
 
1174
1175
  function ymd(d) {
1175
1176
  const yyyy = d.getUTCFullYear();
@@ -1188,6 +1189,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
1188
1189
  function renderAccounting(payload) {
1189
1190
  if (accSummary) accSummary.innerHTML = '';
1190
1191
  if (accRows) accRows.innerHTML = '';
1192
+ if (accFreeRows) accFreeRows.innerHTML = '';
1191
1193
  if (!payload || !payload.ok) {
1192
1194
  return;
1193
1195
  }
@@ -1207,8 +1209,13 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
1207
1209
  const user = r.username ? `<a href="/user/${encodeURIComponent(r.username)}" target="_blank">${escapeHtml(r.username)}</a>${r.isFree ? ' <em>(gratuit)</em>' : ''}` : (r.isFree ? '<em>(gratuit)</em>' : '');
1208
1210
  const items = Array.isArray(r.items) ? r.items.map((x) => escapeHtml(x)).join('<br>') : '';
1209
1211
  const totalCell = r.isFree ? '-' : escapeHtml((Number(r.total) || 0).toFixed(2));
1210
- tr.innerHTML = `<td>${escapeHtml(r.startDate)} → ${escapeHtml(r.endDate)}</td><td>${user}</td><td>${items}</td><td>${totalCell}</td><td><code>${escapeHtml(r.rid)}</code></td>`;
1211
- accRows && accRows.appendChild(tr);
1212
+ if (r.isFree) {
1213
+ tr.innerHTML = `<td>${escapeHtml(r.startDate)} → ${escapeHtml(r.endDate)}</td><td>${user}</td><td>${items}</td><td><code>${escapeHtml(r.rid)}</code></td>`;
1214
+ accFreeRows && accFreeRows.appendChild(tr);
1215
+ } else {
1216
+ tr.innerHTML = `<td>${escapeHtml(r.startDate)} → ${escapeHtml(r.endDate)}</td><td>${user}</td><td>${items}</td><td>${totalCell}</td><td><code>${escapeHtml(r.rid)}</code></td>`;
1217
+ accRows && accRows.appendChild(tr);
1218
+ }
1212
1219
  });
1213
1220
  }
1214
1221
 
@@ -2033,20 +2033,22 @@ function toDatetimeLocalValue(date) {
2033
2033
  async function openFabDatePicker() {
2034
2034
  if (!lockAction('fab-date-picker', 700)) return;
2035
2035
 
2036
- // Defaults: today -> today (1 day booking)
2036
+ // Defaults: tomorrow -> tomorrow (cannot book today or past)
2037
2037
  const today = new Date();
2038
2038
  today.setHours(0, 0, 0, 0);
2039
+ const minStart = new Date(today);
2040
+ minStart.setDate(minStart.getDate() + 1);
2039
2041
 
2040
2042
  const html = `
2041
2043
  <div class="onekite-fab-dates">
2042
2044
  <div class="mb-2">
2043
2045
  <label class="form-label">Date de début</label>
2044
- <input class="form-control" type="text" id="onekite-fab-start" placeholder="dd/mm/yyyy" autocomplete="off" />
2046
+ <input class="form-control" type="date" id="onekite-fab-start" autocomplete="off" />
2045
2047
  </div>
2046
2048
  <div class="mb-2">
2047
2049
  <label class="form-label">Date de fin (incluse)</label>
2048
- <input class="form-control" type="text" id="onekite-fab-end" placeholder="dd/mm/yyyy" autocomplete="off" />
2049
- <div class="form-text">La date de fin est incluse (ex: 13/01/2026 → 13/01/2026 = 1 jour).</div>
2050
+ <input class="form-control" type="date" id="onekite-fab-end" autocomplete="off" />
2051
+ <div class="form-text">Sélectionne une période (la date de fin est incluse).</div>
2050
2052
  </div>
2051
2053
  </div>
2052
2054
  `;
@@ -2060,10 +2062,18 @@ function toDatetimeLocalValue(date) {
2060
2062
  label: 'Continuer',
2061
2063
  className: 'btn-primary',
2062
2064
  callback: function () {
2063
- const s = parseDdMmYyyy(document.getElementById('onekite-fab-start').value);
2064
- const e = parseDdMmYyyy(document.getElementById('onekite-fab-end').value);
2065
+ const sStr = String(document.getElementById('onekite-fab-start').value || '');
2066
+ const eStr = String(document.getElementById('onekite-fab-end').value || '');
2067
+ const s = parseYmdDate(sStr);
2068
+ const e = parseYmdDate(eStr);
2065
2069
  if (!s || !e) {
2066
- alerts.error('Dates invalides. Format attendu : dd/mm/yyyy');
2070
+ alerts.error('Dates invalides.');
2071
+ return false;
2072
+ }
2073
+
2074
+ // Cannot book today or past
2075
+ if (s < minStart || e < minStart) {
2076
+ alerts.error('Impossible de réserver pour le jour même ou dans le passé.');
2067
2077
  return false;
2068
2078
  }
2069
2079
  if (e < s) {
@@ -2118,13 +2128,46 @@ function toDatetimeLocalValue(date) {
2118
2128
  try {
2119
2129
  const startEl = document.getElementById('onekite-fab-start');
2120
2130
  const endEl = document.getElementById('onekite-fab-end');
2121
- if (startEl) startEl.value = formatDdMmYyyy(today);
2122
- if (endEl) endEl.value = formatDdMmYyyy(today);
2131
+ const minStr = toLocalYmd(minStart);
2132
+ if (startEl) {
2133
+ startEl.min = minStr;
2134
+ startEl.value = minStr;
2135
+ }
2136
+ if (endEl) {
2137
+ endEl.min = minStr;
2138
+ endEl.value = minStr;
2139
+ }
2140
+ if (startEl && endEl) {
2141
+ startEl.addEventListener('change', () => {
2142
+ try {
2143
+ const d = parseYmdDate(String(startEl.value || ''));
2144
+ if (!d) return;
2145
+ const minEnd = new Date(d);
2146
+ const minEndStr = toLocalYmd(minEnd);
2147
+ endEl.min = minEndStr;
2148
+ if (String(endEl.value || '') < minEndStr) endEl.value = minEndStr;
2149
+ } catch (e) {}
2150
+ });
2151
+ }
2123
2152
  if (startEl) startEl.focus();
2124
2153
  } catch (e) {}
2125
2154
  });
2126
2155
  }
2127
2156
 
2157
+ function parseYmdDate(ymdStr) {
2158
+ // Expect YYYY-MM-DD (from <input type="date">)
2159
+ if (!ymdStr || typeof ymdStr !== 'string') return null;
2160
+ const m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(ymdStr);
2161
+ if (!m) return null;
2162
+ const y = parseInt(m[1], 10);
2163
+ const mo = parseInt(m[2], 10);
2164
+ const d = parseInt(m[3], 10);
2165
+ if (!y || !mo || !d) return null;
2166
+ const dt = new Date(y, mo - 1, d);
2167
+ dt.setHours(0, 0, 0, 0);
2168
+ return dt;
2169
+ }
2170
+
2128
2171
  function mountMobileFab() {
2129
2172
  try {
2130
2173
  unmountMobileFab();
@@ -219,6 +219,17 @@
219
219
  <tbody></tbody>
220
220
  </table>
221
221
  </div>
222
+
223
+ <h5 class="mt-4">Détails des sorties gratuites</h5>
224
+ <div class="form-text mb-2">Réservations gratuites (validateurs) : comptées séparément (hors chiffre d’affaires).</div>
225
+ <div class="table-responsive">
226
+ <table class="table table-sm" id="onekite-acc-free-rows">
227
+ <thead>
228
+ <tr><th>Date</th><th>Utilisateur</th><th>Matériel</th><th>RID</th></tr>
229
+ </thead>
230
+ <tbody></tbody>
231
+ </table>
232
+ </div>
222
233
  </div>
223
234
 
224
235
  <div class="tab-pane fade" id="onekite-tab-maintenance" role="tabpanel">
package/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.12"
42
+ "version": "2.0.13"
43
43
  }
package/public/admin.js CHANGED
@@ -1170,6 +1170,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
1170
1170
  const accPurge = document.getElementById('onekite-acc-purge');
1171
1171
  const accSummary = document.querySelector('#onekite-acc-summary tbody');
1172
1172
  const accRows = document.querySelector('#onekite-acc-rows tbody');
1173
+ const accFreeRows = document.querySelector('#onekite-acc-free-rows tbody');
1173
1174
 
1174
1175
  function ymd(d) {
1175
1176
  const yyyy = d.getUTCFullYear();
@@ -1188,6 +1189,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
1188
1189
  function renderAccounting(payload) {
1189
1190
  if (accSummary) accSummary.innerHTML = '';
1190
1191
  if (accRows) accRows.innerHTML = '';
1192
+ if (accFreeRows) accFreeRows.innerHTML = '';
1191
1193
  if (!payload || !payload.ok) {
1192
1194
  return;
1193
1195
  }
@@ -1207,8 +1209,13 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
1207
1209
  const user = r.username ? `<a href="/user/${encodeURIComponent(r.username)}" target="_blank">${escapeHtml(r.username)}</a>${r.isFree ? ' <em>(gratuit)</em>' : ''}` : (r.isFree ? '<em>(gratuit)</em>' : '');
1208
1210
  const items = Array.isArray(r.items) ? r.items.map((x) => escapeHtml(x)).join('<br>') : '';
1209
1211
  const totalCell = r.isFree ? '-' : escapeHtml((Number(r.total) || 0).toFixed(2));
1210
- tr.innerHTML = `<td>${escapeHtml(r.startDate)} → ${escapeHtml(r.endDate)}</td><td>${user}</td><td>${items}</td><td>${totalCell}</td><td><code>${escapeHtml(r.rid)}</code></td>`;
1211
- accRows && accRows.appendChild(tr);
1212
+ if (r.isFree) {
1213
+ tr.innerHTML = `<td>${escapeHtml(r.startDate)} → ${escapeHtml(r.endDate)}</td><td>${user}</td><td>${items}</td><td><code>${escapeHtml(r.rid)}</code></td>`;
1214
+ accFreeRows && accFreeRows.appendChild(tr);
1215
+ } else {
1216
+ tr.innerHTML = `<td>${escapeHtml(r.startDate)} → ${escapeHtml(r.endDate)}</td><td>${user}</td><td>${items}</td><td>${totalCell}</td><td><code>${escapeHtml(r.rid)}</code></td>`;
1217
+ accRows && accRows.appendChild(tr);
1218
+ }
1212
1219
  });
1213
1220
  }
1214
1221
 
package/public/client.js CHANGED
@@ -2033,20 +2033,22 @@ function toDatetimeLocalValue(date) {
2033
2033
  async function openFabDatePicker() {
2034
2034
  if (!lockAction('fab-date-picker', 700)) return;
2035
2035
 
2036
- // Defaults: today -> today (1 day booking)
2036
+ // Defaults: tomorrow -> tomorrow (cannot book today or past)
2037
2037
  const today = new Date();
2038
2038
  today.setHours(0, 0, 0, 0);
2039
+ const minStart = new Date(today);
2040
+ minStart.setDate(minStart.getDate() + 1);
2039
2041
 
2040
2042
  const html = `
2041
2043
  <div class="onekite-fab-dates">
2042
2044
  <div class="mb-2">
2043
2045
  <label class="form-label">Date de début</label>
2044
- <input class="form-control" type="text" id="onekite-fab-start" placeholder="dd/mm/yyyy" autocomplete="off" />
2046
+ <input class="form-control" type="date" id="onekite-fab-start" autocomplete="off" />
2045
2047
  </div>
2046
2048
  <div class="mb-2">
2047
2049
  <label class="form-label">Date de fin (incluse)</label>
2048
- <input class="form-control" type="text" id="onekite-fab-end" placeholder="dd/mm/yyyy" autocomplete="off" />
2049
- <div class="form-text">La date de fin est incluse (ex: 13/01/2026 → 13/01/2026 = 1 jour).</div>
2050
+ <input class="form-control" type="date" id="onekite-fab-end" autocomplete="off" />
2051
+ <div class="form-text">Sélectionne une période (la date de fin est incluse).</div>
2050
2052
  </div>
2051
2053
  </div>
2052
2054
  `;
@@ -2060,10 +2062,18 @@ function toDatetimeLocalValue(date) {
2060
2062
  label: 'Continuer',
2061
2063
  className: 'btn-primary',
2062
2064
  callback: function () {
2063
- const s = parseDdMmYyyy(document.getElementById('onekite-fab-start').value);
2064
- const e = parseDdMmYyyy(document.getElementById('onekite-fab-end').value);
2065
+ const sStr = String(document.getElementById('onekite-fab-start').value || '');
2066
+ const eStr = String(document.getElementById('onekite-fab-end').value || '');
2067
+ const s = parseYmdDate(sStr);
2068
+ const e = parseYmdDate(eStr);
2065
2069
  if (!s || !e) {
2066
- alerts.error('Dates invalides. Format attendu : dd/mm/yyyy');
2070
+ alerts.error('Dates invalides.');
2071
+ return false;
2072
+ }
2073
+
2074
+ // Cannot book today or past
2075
+ if (s < minStart || e < minStart) {
2076
+ alerts.error('Impossible de réserver pour le jour même ou dans le passé.');
2067
2077
  return false;
2068
2078
  }
2069
2079
  if (e < s) {
@@ -2118,13 +2128,46 @@ function toDatetimeLocalValue(date) {
2118
2128
  try {
2119
2129
  const startEl = document.getElementById('onekite-fab-start');
2120
2130
  const endEl = document.getElementById('onekite-fab-end');
2121
- if (startEl) startEl.value = formatDdMmYyyy(today);
2122
- if (endEl) endEl.value = formatDdMmYyyy(today);
2131
+ const minStr = toLocalYmd(minStart);
2132
+ if (startEl) {
2133
+ startEl.min = minStr;
2134
+ startEl.value = minStr;
2135
+ }
2136
+ if (endEl) {
2137
+ endEl.min = minStr;
2138
+ endEl.value = minStr;
2139
+ }
2140
+ if (startEl && endEl) {
2141
+ startEl.addEventListener('change', () => {
2142
+ try {
2143
+ const d = parseYmdDate(String(startEl.value || ''));
2144
+ if (!d) return;
2145
+ const minEnd = new Date(d);
2146
+ const minEndStr = toLocalYmd(minEnd);
2147
+ endEl.min = minEndStr;
2148
+ if (String(endEl.value || '') < minEndStr) endEl.value = minEndStr;
2149
+ } catch (e) {}
2150
+ });
2151
+ }
2123
2152
  if (startEl) startEl.focus();
2124
2153
  } catch (e) {}
2125
2154
  });
2126
2155
  }
2127
2156
 
2157
+ function parseYmdDate(ymdStr) {
2158
+ // Expect YYYY-MM-DD (from <input type="date">)
2159
+ if (!ymdStr || typeof ymdStr !== 'string') return null;
2160
+ const m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(ymdStr);
2161
+ if (!m) return null;
2162
+ const y = parseInt(m[1], 10);
2163
+ const mo = parseInt(m[2], 10);
2164
+ const d = parseInt(m[3], 10);
2165
+ if (!y || !mo || !d) return null;
2166
+ const dt = new Date(y, mo - 1, d);
2167
+ dt.setHours(0, 0, 0, 0);
2168
+ return dt;
2169
+ }
2170
+
2128
2171
  function mountMobileFab() {
2129
2172
  try {
2130
2173
  unmountMobileFab();
@@ -219,6 +219,17 @@
219
219
  <tbody></tbody>
220
220
  </table>
221
221
  </div>
222
+
223
+ <h5 class="mt-4">Détails des sorties gratuites</h5>
224
+ <div class="form-text mb-2">Réservations gratuites (validateurs) : comptées séparément (hors chiffre d’affaires).</div>
225
+ <div class="table-responsive">
226
+ <table class="table table-sm" id="onekite-acc-free-rows">
227
+ <thead>
228
+ <tr><th>Date</th><th>Utilisateur</th><th>Matériel</th><th>RID</th></tr>
229
+ </thead>
230
+ <tbody></tbody>
231
+ </table>
232
+ </div>
222
233
  </div>
223
234
 
224
235
  <div class="tab-pane fade" id="onekite-tab-maintenance" role="tabpanel">