nodebb-plugin-onekite-calendar 2.0.12 → 2.0.14
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 +10 -0
- package/lib/admin.js +11 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/admin.js +19 -2
- package/public/client.js +237 -84
- package/templates/admin/plugins/calendar-onekite.tpl +20 -0
- package/pkg/package/CHANGELOG.md +0 -102
- package/pkg/package/lib/admin.js +0 -554
- package/pkg/package/lib/api.js +0 -1458
- package/pkg/package/lib/controllers.js +0 -11
- package/pkg/package/lib/db.js +0 -224
- package/pkg/package/lib/discord.js +0 -190
- package/pkg/package/lib/helloasso.js +0 -352
- package/pkg/package/lib/helloassoWebhook.js +0 -389
- package/pkg/package/lib/scheduler.js +0 -201
- package/pkg/package/lib/widgets.js +0 -460
- package/pkg/package/library.js +0 -164
- package/pkg/package/package.json +0 -14
- package/pkg/package/plugin.json +0 -43
- package/pkg/package/public/admin.js +0 -1470
- package/pkg/package/public/client.js +0 -2185
- package/pkg/package/templates/admin/plugins/calendar-onekite.tpl +0 -287
- package/pkg/package/templates/calendar-onekite.tpl +0 -51
- package/pkg/package/templates/emails/calendar-onekite_approved.tpl +0 -40
- package/pkg/package/templates/emails/calendar-onekite_cancelled.tpl +0 -15
- package/pkg/package/templates/emails/calendar-onekite_expired.tpl +0 -11
- package/pkg/package/templates/emails/calendar-onekite_paid.tpl +0 -15
- package/pkg/package/templates/emails/calendar-onekite_pending.tpl +0 -15
- package/pkg/package/templates/emails/calendar-onekite_refused.tpl +0 -15
- package/pkg/package/templates/emails/calendar-onekite_reminder.tpl +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog – calendar-onekite
|
|
2
2
|
|
|
3
|
+
## 1.3.8
|
|
4
|
+
- ACP Comptabilisation : ajout d’un **grand total** (montant total des locations payées sur la période) + compteurs (payées / sorties gratuites).
|
|
5
|
+
|
|
6
|
+
## 1.3.7
|
|
7
|
+
- Mobile FAB : remplacement des 2 champs date par un **sélecteur unique** (calendrier) permettant de choisir directement une plage (clic début puis fin, fin incluse), avec dates passées/aujourd'hui désactivées.
|
|
8
|
+
|
|
9
|
+
## 1.3.6
|
|
10
|
+
- ACP Comptabilisation : ajout d’un tableau séparé « Détails des sorties gratuites » (avec le nom du matériel).
|
|
11
|
+
- 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é.
|
|
12
|
+
|
|
3
13
|
## 1.3.5
|
|
4
14
|
- 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
15
|
|
package/lib/admin.js
CHANGED
|
@@ -405,6 +405,8 @@ admin.getAccounting = async function (req, res) {
|
|
|
405
405
|
const rows = [];
|
|
406
406
|
const byItem = new Map();
|
|
407
407
|
let freeCount = 0;
|
|
408
|
+
let paidCount = 0;
|
|
409
|
+
let grandTotal = 0;
|
|
408
410
|
|
|
409
411
|
for (const rid of ids) {
|
|
410
412
|
const r = await dbLayer.getReservation(rid);
|
|
@@ -424,7 +426,12 @@ admin.getAccounting = async function (req, res) {
|
|
|
424
426
|
const startDate = formatFR(r.start);
|
|
425
427
|
const endDate = formatFR(r.end);
|
|
426
428
|
|
|
427
|
-
if (isFree)
|
|
429
|
+
if (isFree) {
|
|
430
|
+
freeCount += 1;
|
|
431
|
+
} else {
|
|
432
|
+
paidCount += 1;
|
|
433
|
+
grandTotal += total;
|
|
434
|
+
}
|
|
428
435
|
|
|
429
436
|
rows.push({
|
|
430
437
|
rid: r.rid,
|
|
@@ -463,6 +470,9 @@ admin.getAccounting = async function (req, res) {
|
|
|
463
470
|
ok: true,
|
|
464
471
|
from: new Date(minTs).toISOString().slice(0, 10),
|
|
465
472
|
to: new Date(maxTs).toISOString().slice(0, 10),
|
|
473
|
+
paidCount,
|
|
474
|
+
freeCount,
|
|
475
|
+
grandTotal,
|
|
466
476
|
summary,
|
|
467
477
|
rows,
|
|
468
478
|
});
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -1170,6 +1170,10 @@ 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');
|
|
1174
|
+
const accGrandTotal = document.getElementById('onekite-acc-grand-total');
|
|
1175
|
+
const accPaidCount = document.getElementById('onekite-acc-paid-count');
|
|
1176
|
+
const accFreeCount = document.getElementById('onekite-acc-free-count');
|
|
1173
1177
|
|
|
1174
1178
|
function ymd(d) {
|
|
1175
1179
|
const yyyy = d.getUTCFullYear();
|
|
@@ -1188,10 +1192,18 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1188
1192
|
function renderAccounting(payload) {
|
|
1189
1193
|
if (accSummary) accSummary.innerHTML = '';
|
|
1190
1194
|
if (accRows) accRows.innerHTML = '';
|
|
1195
|
+
if (accFreeRows) accFreeRows.innerHTML = '';
|
|
1196
|
+
if (accGrandTotal) accGrandTotal.textContent = '0.00';
|
|
1197
|
+
if (accPaidCount) accPaidCount.textContent = '0';
|
|
1198
|
+
if (accFreeCount) accFreeCount.textContent = '0';
|
|
1191
1199
|
if (!payload || !payload.ok) {
|
|
1192
1200
|
return;
|
|
1193
1201
|
}
|
|
1194
1202
|
|
|
1203
|
+
if (accGrandTotal) accGrandTotal.textContent = (Number(payload.grandTotal) || 0).toFixed(2);
|
|
1204
|
+
if (accPaidCount) accPaidCount.textContent = String(payload.paidCount || 0);
|
|
1205
|
+
if (accFreeCount) accFreeCount.textContent = String(payload.freeCount || 0);
|
|
1206
|
+
|
|
1195
1207
|
(payload.summary || []).forEach((s) => {
|
|
1196
1208
|
const tr = document.createElement('tr');
|
|
1197
1209
|
if (s.isFree) {
|
|
@@ -1207,8 +1219,13 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1207
1219
|
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
1220
|
const items = Array.isArray(r.items) ? r.items.map((x) => escapeHtml(x)).join('<br>') : '';
|
|
1209
1221
|
const totalCell = r.isFree ? '-' : escapeHtml((Number(r.total) || 0).toFixed(2));
|
|
1210
|
-
|
|
1211
|
-
|
|
1222
|
+
if (r.isFree) {
|
|
1223
|
+
tr.innerHTML = `<td>${escapeHtml(r.startDate)} → ${escapeHtml(r.endDate)}</td><td>${user}</td><td>${items}</td><td><code>${escapeHtml(r.rid)}</code></td>`;
|
|
1224
|
+
accFreeRows && accFreeRows.appendChild(tr);
|
|
1225
|
+
} else {
|
|
1226
|
+
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>`;
|
|
1227
|
+
accRows && accRows.appendChild(tr);
|
|
1228
|
+
}
|
|
1212
1229
|
});
|
|
1213
1230
|
}
|
|
1214
1231
|
|
package/public/client.js
CHANGED
|
@@ -61,6 +61,21 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
61
61
|
font-size: 20px;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/* Mobile FAB date range picker (single calendar) */
|
|
66
|
+
.onekite-range-picker { user-select: none; }
|
|
67
|
+
.onekite-range-header { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px; }
|
|
68
|
+
.onekite-range-header .btn { padding:.25rem .5rem; }
|
|
69
|
+
.onekite-range-month { font-weight:600; }
|
|
70
|
+
.onekite-range-weekdays, .onekite-range-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:4px; }
|
|
71
|
+
.onekite-range-weekdays div { text-align:center; font-size:.8rem; opacity:.7; padding:2px 0; }
|
|
72
|
+
.onekite-range-day { border:1px solid rgba(0,0,0,.08); border-radius:10px; padding:8px 0; text-align:center; cursor:pointer; background:var(--bs-body-bg,#fff); color:var(--bs-body-color,#212529); }
|
|
73
|
+
.onekite-range-day.is-empty { border-color:transparent; background:transparent; cursor:default; }
|
|
74
|
+
.onekite-range-day.is-disabled { opacity:.35; cursor:not-allowed; }
|
|
75
|
+
.onekite-range-day.is-start, .onekite-range-day.is-end { border-color:rgba(0,0,0,.18); font-weight:700; }
|
|
76
|
+
.onekite-range-day.is-inrange { background:var(--bs-tertiary-bg, rgba(0,0,0,.04)); }
|
|
77
|
+
.onekite-range-summary { margin-top:10px; font-size:.9rem; }
|
|
78
|
+
|
|
64
79
|
`;
|
|
65
80
|
document.head.appendChild(style);
|
|
66
81
|
} catch (e) {
|
|
@@ -2030,99 +2045,237 @@ function toDatetimeLocalValue(date) {
|
|
|
2030
2045
|
return d;
|
|
2031
2046
|
}
|
|
2032
2047
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2048
|
+
|
|
2049
|
+
async function openFabDatePicker() {
|
|
2050
|
+
if (!lockAction('fab-date-picker', 700)) return;
|
|
2051
|
+
|
|
2052
|
+
// Cannot book today or past (mobile FAB).
|
|
2053
|
+
const today = new Date();
|
|
2054
|
+
today.setHours(0, 0, 0, 0);
|
|
2055
|
+
const minStart = new Date(today);
|
|
2056
|
+
minStart.setDate(minStart.getDate() + 1);
|
|
2057
|
+
|
|
2058
|
+
// Month cursor for the single-calendar range picker
|
|
2059
|
+
const cursor = new Date(minStart);
|
|
2060
|
+
cursor.setDate(1);
|
|
2061
|
+
|
|
2062
|
+
const state = {
|
|
2063
|
+
cursor,
|
|
2064
|
+
minStart,
|
|
2065
|
+
start: null,
|
|
2066
|
+
end: null, // inclusive
|
|
2067
|
+
};
|
|
2039
2068
|
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
<div class="mb-2">
|
|
2047
|
-
<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
|
-
</div>
|
|
2069
|
+
const html = `
|
|
2070
|
+
<div class="onekite-range-picker" id="onekite-range-picker">
|
|
2071
|
+
<div class="onekite-range-header">
|
|
2072
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-prev" aria-label="Mois précédent">‹</button>
|
|
2073
|
+
<div class="onekite-range-month" id="onekite-range-month"></div>
|
|
2074
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-next" aria-label="Mois suivant">›</button>
|
|
2051
2075
|
</div>
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2076
|
+
<div class="onekite-range-weekdays">
|
|
2077
|
+
<div>L</div><div>M</div><div>M</div><div>J</div><div>V</div><div>S</div><div>D</div>
|
|
2078
|
+
</div>
|
|
2079
|
+
<div class="onekite-range-grid" id="onekite-range-grid"></div>
|
|
2080
|
+
<div class="onekite-range-summary" id="onekite-range-summary"></div>
|
|
2081
|
+
<div class="form-text mt-2">Sélectionne une date de début puis une date de fin (incluse). Les dates d'aujourd'hui et passées sont désactivées.</div>
|
|
2082
|
+
</div>
|
|
2083
|
+
`;
|
|
2084
|
+
|
|
2085
|
+
const dlg = bootbox.dialog({
|
|
2086
|
+
title: 'Choisir une période',
|
|
2087
|
+
message: html,
|
|
2088
|
+
buttons: {
|
|
2089
|
+
cancel: { label: 'Annuler', className: 'btn-secondary' },
|
|
2090
|
+
ok: {
|
|
2091
|
+
label: 'Continuer',
|
|
2092
|
+
className: 'btn-primary',
|
|
2093
|
+
callback: function () {
|
|
2094
|
+
const s = state.start;
|
|
2095
|
+
const e = state.end || state.start;
|
|
2096
|
+
if (!s) {
|
|
2097
|
+
alerts.error('Choisis une date de début.');
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
if (!e) {
|
|
2101
|
+
alerts.error('Choisis une date de fin.');
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
// Convert end inclusive -> end exclusive (FullCalendar rule)
|
|
2105
|
+
const endExcl = new Date(e);
|
|
2106
|
+
endExcl.setDate(endExcl.getDate() + 1);
|
|
2077
2107
|
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
2099
|
-
}
|
|
2100
|
-
invalidateEventsCache();
|
|
2101
|
-
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2108
|
+
(async () => {
|
|
2109
|
+
try {
|
|
2110
|
+
if (isDialogOpen) return;
|
|
2111
|
+
isDialogOpen = true;
|
|
2112
|
+
const items = cachedItems || (await loadItems());
|
|
2113
|
+
const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
|
|
2114
|
+
if (chosen && chosen.itemIds && chosen.itemIds.length) {
|
|
2115
|
+
const startDate = toLocalYmd(s);
|
|
2116
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(endExcl);
|
|
2117
|
+
const resp = await requestReservation({
|
|
2118
|
+
start: startDate,
|
|
2119
|
+
end: endDate,
|
|
2120
|
+
itemIds: chosen.itemIds,
|
|
2121
|
+
itemNames: chosen.itemNames,
|
|
2122
|
+
total: chosen.total,
|
|
2123
|
+
});
|
|
2124
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
2125
|
+
showAlert('success', 'Réservation confirmée.');
|
|
2126
|
+
} else {
|
|
2127
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
2102
2128
|
}
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
} finally {
|
|
2106
|
-
isDialogOpen = false;
|
|
2129
|
+
invalidateEventsCache();
|
|
2130
|
+
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2107
2131
|
}
|
|
2108
|
-
}
|
|
2132
|
+
} catch (err) {
|
|
2133
|
+
// ignore
|
|
2134
|
+
} finally {
|
|
2135
|
+
isDialogOpen = false;
|
|
2136
|
+
}
|
|
2137
|
+
})();
|
|
2109
2138
|
|
|
2110
|
-
|
|
2111
|
-
return true;
|
|
2112
|
-
},
|
|
2139
|
+
return true;
|
|
2113
2140
|
},
|
|
2114
2141
|
},
|
|
2115
|
-
}
|
|
2142
|
+
},
|
|
2143
|
+
});
|
|
2116
2144
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2145
|
+
function sameDay(a, b) {
|
|
2146
|
+
return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function clampToMidnight(d) {
|
|
2150
|
+
const x = new Date(d);
|
|
2151
|
+
x.setHours(0, 0, 0, 0);
|
|
2152
|
+
return x;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function monthLabel(d) {
|
|
2156
|
+
try {
|
|
2157
|
+
return d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
|
|
2158
|
+
} catch (e) {
|
|
2159
|
+
return `${d.getMonth() + 1}/${d.getFullYear()}`;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function render() {
|
|
2164
|
+
const monthEl = document.getElementById('onekite-range-month');
|
|
2165
|
+
const gridEl = document.getElementById('onekite-range-grid');
|
|
2166
|
+
const sumEl = document.getElementById('onekite-range-summary');
|
|
2167
|
+
if (!monthEl || !gridEl || !sumEl) return;
|
|
2168
|
+
|
|
2169
|
+
monthEl.textContent = monthLabel(state.cursor);
|
|
2170
|
+
gridEl.innerHTML = '';
|
|
2171
|
+
|
|
2172
|
+
const year = state.cursor.getFullYear();
|
|
2173
|
+
const month = state.cursor.getMonth();
|
|
2174
|
+
const first = new Date(year, month, 1);
|
|
2175
|
+
const last = new Date(year, month + 1, 0);
|
|
2176
|
+
|
|
2177
|
+
// Monday-based week index: 0..6
|
|
2178
|
+
const firstDow = (first.getDay() + 6) % 7;
|
|
2179
|
+
const daysInMonth = last.getDate();
|
|
2180
|
+
|
|
2181
|
+
// Empty cells before month start
|
|
2182
|
+
for (let i = 0; i < firstDow; i++) {
|
|
2183
|
+
const div = document.createElement('div');
|
|
2184
|
+
div.className = 'onekite-range-day is-empty';
|
|
2185
|
+
gridEl.appendChild(div);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
const s = state.start;
|
|
2189
|
+
const e = state.end || state.start;
|
|
2190
|
+
|
|
2191
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
2192
|
+
const d = new Date(year, month, day);
|
|
2193
|
+
d.setHours(0, 0, 0, 0);
|
|
2194
|
+
|
|
2195
|
+
const div = document.createElement('div');
|
|
2196
|
+
div.className = 'onekite-range-day';
|
|
2197
|
+
div.textContent = String(day);
|
|
2198
|
+
|
|
2199
|
+
const disabled = d < state.minStart;
|
|
2200
|
+
if (disabled) div.classList.add('is-disabled');
|
|
2201
|
+
|
|
2202
|
+
if (s && sameDay(d, s)) div.classList.add('is-start');
|
|
2203
|
+
if (state.end && sameDay(d, state.end)) div.classList.add('is-end');
|
|
2204
|
+
|
|
2205
|
+
if (s && e && d > s && d < e) div.classList.add('is-inrange');
|
|
2206
|
+
if (s && e && sameDay(d, s) && !state.end) div.classList.add('is-inrange');
|
|
2207
|
+
|
|
2208
|
+
if (!disabled) {
|
|
2209
|
+
div.addEventListener('click', () => {
|
|
2210
|
+
const clicked = clampToMidnight(d);
|
|
2211
|
+
// First click: set start
|
|
2212
|
+
if (!state.start || (state.start && state.end)) {
|
|
2213
|
+
state.start = clicked;
|
|
2214
|
+
state.end = null;
|
|
2215
|
+
// If cursor month is before selected month, keep cursor
|
|
2216
|
+
render();
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
// Second click: set end (inclusive)
|
|
2220
|
+
if (clicked < state.start) {
|
|
2221
|
+
state.start = clicked;
|
|
2222
|
+
state.end = null;
|
|
2223
|
+
render();
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
state.end = clicked;
|
|
2227
|
+
render();
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
gridEl.appendChild(div);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// Summary
|
|
2235
|
+
if (!state.start) {
|
|
2236
|
+
sumEl.textContent = 'Aucune date sélectionnée.';
|
|
2237
|
+
} else {
|
|
2238
|
+
const startTxt = formatDdMmYyyy(state.start);
|
|
2239
|
+
const endTxt = formatDdMmYyyy(state.end || state.start);
|
|
2240
|
+
sumEl.textContent = (state.end && !sameDay(state.start, state.end))
|
|
2241
|
+
? `Du ${startTxt} au ${endTxt}`
|
|
2242
|
+
: `Le ${startTxt}`;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
dlg.on('shown.bs.modal', () => {
|
|
2247
|
+
try {
|
|
2248
|
+
const prevBtn = document.getElementById('onekite-range-prev');
|
|
2249
|
+
const nextBtn = document.getElementById('onekite-range-next');
|
|
2250
|
+
if (prevBtn) {
|
|
2251
|
+
prevBtn.addEventListener('click', () => {
|
|
2252
|
+
state.cursor.setMonth(state.cursor.getMonth() - 1);
|
|
2253
|
+
render();
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
if (nextBtn) {
|
|
2257
|
+
nextBtn.addEventListener('click', () => {
|
|
2258
|
+
state.cursor.setMonth(state.cursor.getMonth() + 1);
|
|
2259
|
+
render();
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
render();
|
|
2263
|
+
} catch (e) {}
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
function parseYmdDate(ymdStr) {
|
|
2268
|
+
// Expect YYYY-MM-DD (from <input type="date">)
|
|
2269
|
+
if (!ymdStr || typeof ymdStr !== 'string') return null;
|
|
2270
|
+
const m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(ymdStr);
|
|
2271
|
+
if (!m) return null;
|
|
2272
|
+
const y = parseInt(m[1], 10);
|
|
2273
|
+
const mo = parseInt(m[2], 10);
|
|
2274
|
+
const d = parseInt(m[3], 10);
|
|
2275
|
+
if (!y || !mo || !d) return null;
|
|
2276
|
+
const dt = new Date(y, mo - 1, d);
|
|
2277
|
+
dt.setHours(0, 0, 0, 0);
|
|
2278
|
+
return dt;
|
|
2126
2279
|
}
|
|
2127
2280
|
|
|
2128
2281
|
function mountMobileFab() {
|
|
@@ -200,6 +200,15 @@
|
|
|
200
200
|
</div>
|
|
201
201
|
</div>
|
|
202
202
|
|
|
203
|
+
<div class="card mb-3">
|
|
204
|
+
<div class="card-body py-3">
|
|
205
|
+
<div class="d-flex flex-wrap gap-3 align-items-baseline">
|
|
206
|
+
<div><strong>Grand total :</strong> <span id="onekite-acc-grand-total">0.00</span> €</div>
|
|
207
|
+
<div class="text-muted">(<span id="onekite-acc-paid-count">0</span> location(s) payée(s) • <span id="onekite-acc-free-count">0</span> sortie(s) gratuite(s))</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
203
212
|
<h5>Résumé par matériel</h5>
|
|
204
213
|
<div class="table-responsive">
|
|
205
214
|
<table class="table table-sm" id="onekite-acc-summary">
|
|
@@ -219,6 +228,17 @@
|
|
|
219
228
|
<tbody></tbody>
|
|
220
229
|
</table>
|
|
221
230
|
</div>
|
|
231
|
+
|
|
232
|
+
<h5 class="mt-4">Détails des sorties gratuites</h5>
|
|
233
|
+
<div class="form-text mb-2">Réservations gratuites (validateurs) : comptées séparément (hors chiffre d’affaires).</div>
|
|
234
|
+
<div class="table-responsive">
|
|
235
|
+
<table class="table table-sm" id="onekite-acc-free-rows">
|
|
236
|
+
<thead>
|
|
237
|
+
<tr><th>Date</th><th>Utilisateur</th><th>Matériel</th><th>RID</th></tr>
|
|
238
|
+
</thead>
|
|
239
|
+
<tbody></tbody>
|
|
240
|
+
</table>
|
|
241
|
+
</div>
|
|
222
242
|
</div>
|
|
223
243
|
|
|
224
244
|
<div class="tab-pane fade" id="onekite-tab-maintenance" role="tabpanel">
|
package/pkg/package/CHANGELOG.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# Changelog – calendar-onekite
|
|
2
|
-
|
|
3
|
-
## 1.3.5
|
|
4
|
-
- 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
|
-
|
|
6
|
-
## 1.3.4
|
|
7
|
-
- Réservations gratuites (validateurs) : ne sont plus comptées comme chiffre d’affaires dans la comptabilisation. Elles apparaissent désormais sur une ligne séparée « Sorties gratuites » (et sont marquées (gratuit) dans le détail).
|
|
8
|
-
|
|
9
|
-
## 1.3.3
|
|
10
|
-
- Comptabilité : les réservations « auto-checkées » (réservations faites par un validateur) recalculent désormais le total côté serveur (catalogue HelloAsso × nb jours calendaires) afin d’être comptabilisées correctement.
|
|
11
|
-
|
|
12
|
-
## 1.3.2
|
|
13
|
-
- Les membres validateurs (ceux qui peuvent valider/supprimer une demande) voient leurs propres réservations passer directement en statut payé/checked (pas de workflow paiement).
|
|
14
|
-
- UI : message de succès adapté (réservation confirmée).
|
|
15
|
-
|
|
16
|
-
## 1.3.1
|
|
17
|
-
- ACP : ajout de 2 actions rapides dans l’onglet Maintenance : « Tout mettre en maintenance » et « Tout enlever de maintenance » (avec audit).
|
|
18
|
-
|
|
19
|
-
## 1.3.0
|
|
20
|
-
- Maintenance (sans dates) : blocage manuel ON/OFF par matériel (ACP + API). Les matériels en maintenance sont grisés et affichés comme : 🔧 Nom (en maintenance).
|
|
21
|
-
- Audit : journal des actions (demande, validation, refus, annulation, maintenance) avec consultation et purge par année (ACP + API).
|
|
22
|
-
|
|
23
|
-
## 1.2.18
|
|
24
|
-
- API events + anti double booking : les tests de chevauchement utilisent désormais en priorité startDate/endDate (YYYY-MM-DD) quand disponibles (logique calendaire pure, endDate exclusive). Cela supprime définitivement les faux chevauchements liés aux timestamps/fuseaux/DST, notamment sur mobile et « Durée rapide ».
|
|
25
|
-
|
|
26
|
-
## 1.2.17
|
|
27
|
-
- Modale réservation : la requête de disponibilité initiale utilise aussi des dates calendaires (YYYY-MM-DD) au lieu de startStr/endStr/toISOString(), ce qui corrige le grisé erroné (mobile + durée rapide).
|
|
28
|
-
|
|
29
|
-
## 1.2.16
|
|
30
|
-
- Modale réservation (Durée rapide) : correction du grisé erroné des matériels réservés la veille. Les requêtes de disponibilité utilisent désormais des dates calendaires (YYYY-MM-DD) au lieu de toISOString() (UTC), pour éviter tout faux chevauchement lié au fuseau/DST.
|
|
31
|
-
|
|
32
|
-
## 1.2.15
|
|
33
|
-
- Réservations (Durée rapide) : génération des évènements all-day basée sur startDate/endDate (YYYY-MM-DD) quand disponibles, pour éviter tout décalage lié à toISOString() (UTC) et empêcher un grisé « non disponible » le jour suivant.
|
|
34
|
-
|
|
35
|
-
## 1.2.14
|
|
36
|
-
- Réservations : correction d’un faux chevauchement (problème 1h) quand FullCalendar envoie des bornes all-day à minuit UTC (Z / +00:00) — le lendemain n’est plus marqué « non disponible ».
|
|
37
|
-
|
|
38
|
-
## 1.2.13
|
|
39
|
-
- ACP (mode sombre) : correction de la visibilité de la liste d’autocomplete d’adresse (couleurs via variables Bootstrap)
|
|
40
|
-
|
|
41
|
-
## 1.2.12
|
|
42
|
-
- Modales (calendrier + ACP) : autocomplete adresse rendu identique et compatible Bootstrap input-group (plus de wrapper qui casse l’affichage)
|
|
43
|
-
|
|
44
|
-
## 1.2.11
|
|
45
|
-
- ACP : ajout de la recherche automatique d’adresse (autocomplete Nominatim) dans la modale de validation (unitaire + batch), comme sur le calendrier
|
|
46
|
-
|
|
47
|
-
## 1.2.10
|
|
48
|
-
- HelloAsso : calcul des jours 100% fiable (différence en jours calendaires Y/M/J, sans dépendance aux heures/au fuseau/DST)
|
|
49
|
-
- FullCalendar : endDate traitée comme exclusive partout (UI + checkout)
|
|
50
|
-
- HelloAsso : montant du checkout recalculé côté serveur à partir du catalogue (prix/jour × nbJours)
|
|
51
|
-
|
|
52
|
-
## 1.2.9
|
|
53
|
-
- Modale réservation : retour du grisé des matériels indisponibles (API events expose à nouveau itemIds)
|
|
54
|
-
|
|
55
|
-
## 1.2.8
|
|
56
|
-
- Popup réservation : correction « Durée rapide » (la période envoyée correspond bien à la durée sélectionnée)
|
|
57
|
-
|
|
58
|
-
## 1.2.7
|
|
59
|
-
- UI : suppression du bouton flottant mobile « + Réserver »
|
|
60
|
-
- Client : nettoyage du code associé (suppression du bloc FAB)
|
|
61
|
-
|
|
62
|
-
## 1.2.6
|
|
63
|
-
- ACP : anti double action (verrou UI + boutons désactivés/spinner) sur valider/refuser (unitaire + batch)
|
|
64
|
-
|
|
65
|
-
## 1.2.5
|
|
66
|
-
- Mobile : bouton flottant « + Réserver »
|
|
67
|
-
- Création : anti double-tap/click (verrou actions) + invalidation cache events + refetch léger
|
|
68
|
-
- Calendrier : jours passés / aujourd’hui affichés comme non-sélectionnables (règle visuelle)
|
|
69
|
-
- Popup réservation : raccourcis de durée (1j/2j/3j/7j) + recalcul total + mise à jour des matériels bloqués
|
|
70
|
-
- ACP : actions en batch (valider/refuser une sélection) + compteur de sélection
|
|
71
|
-
- API : idempotence sur valider/refuser + audit enrichi (refusedBy, cancelledByUsername)
|
|
72
|
-
|
|
73
|
-
## 1.0.3
|
|
74
|
-
- Suppression du texte d’archivage dans le toast de purge (plus de « 0 archivés »)
|
|
75
|
-
- Renommage du plugin : nodebb-plugin-onekite-calendar
|
|
76
|
-
- Discord : notification « ❌ Réservation annulée » (annulation manuelle + annulation automatique) + option ACP
|
|
77
|
-
|
|
78
|
-
## 1.0.2
|
|
79
|
-
- Purge calendrier : suppression réelle des réservations (aucune logique d’archivage)
|
|
80
|
-
- Compta conservée séparément (la purge n’y touche jamais)
|
|
81
|
-
|
|
82
|
-
## 1.0.1
|
|
83
|
-
- ACP : modales Valider / Refuser OK (plus de retour au premier onglet)
|
|
84
|
-
- Scheduler : optimisation (batch DB, scans réduits)
|
|
85
|
-
- API events : pagination interne + ETag optimisé
|
|
86
|
-
- HelloAsso : token mis en cache + protection rate-limit (429 / Cloudflare 1015), sans logs supplémentaires
|
|
87
|
-
- Discord : notifications en embeds + icônes ⏳ (demande) et 💳 (paiement)
|
|
88
|
-
- Widget : suppression de “2 semaines” dans le titre affiché
|
|
89
|
-
- Correctifs divers de stabilité/performance (dont crash au démarrage)
|
|
90
|
-
|
|
91
|
-
## 1.0.0
|
|
92
|
-
- Première version stable du plugin calendar-onekite
|
|
93
|
-
- Gestion des réservations de matériel avec contrôle de disponibilité
|
|
94
|
-
- Calendrier FullCalendar via CDN
|
|
95
|
-
- Validation / refus des demandes depuis l’ACP
|
|
96
|
-
- Notifications Discord
|
|
97
|
-
- Intégration paiements HelloAsso
|
|
98
|
-
|
|
99
|
-
## 1.2.19
|
|
100
|
-
- Mobile: ajout d’un bouton flottant (FAB) sur la page calendrier uniquement.
|
|
101
|
-
- Le FAB ouvre une mini-modale de sélection de dates (dd/mm/yyyy) puis ouvre la modale standard de réservation.
|
|
102
|
-
- Le FAB est automatiquement retiré quand on navigue hors de la page calendrier.
|