nodebb-plugin-onekite-calendar 2.0.13 → 2.0.15
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/pkg/nodebb-plugin-onekite-calendar-1.3.9.tgz +0 -0
- package/pkg/package/CHANGELOG.md +6 -0
- package/pkg/package/lib/admin.js +11 -1
- package/pkg/package/package.json +1 -1
- package/pkg/package/plugin.json +1 -1
- package/pkg/package/public/admin.js +10 -0
- package/pkg/package/public/client.js +238 -112
- package/pkg/package/templates/admin/plugins/calendar-onekite.tpl +9 -0
- package/plugin.json +1 -1
- package/public/admin.js +10 -0
- package/public/client.js +238 -112
- package/templates/admin/plugins/calendar-onekite.tpl +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog – calendar-onekite
|
|
2
2
|
|
|
3
|
+
## 1.3.9
|
|
4
|
+
- Mobile FAB : correction de la sélection de plage (tous les jours sélectionnés sont désormais bien inclus dans la surbrillance).
|
|
5
|
+
- Mobile FAB : amélioration du rendu en mode sombre (bordures/fonds adaptés, contraste renforcé).
|
|
6
|
+
|
|
7
|
+
## 1.3.8
|
|
8
|
+
- ACP Comptabilisation : ajout d’un **grand total** (montant total des locations payées sur la période) + compteurs (payées / sorties gratuites).
|
|
9
|
+
|
|
10
|
+
## 1.3.7
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
3
13
|
## 1.3.6
|
|
4
14
|
- ACP Comptabilisation : ajout d’un tableau séparé « Détails des sorties gratuites » (avec le nom du matériel).
|
|
5
15
|
- 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é.
|
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
|
Binary file
|
package/pkg/package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 1.3.6
|
|
4
10
|
- ACP Comptabilisation : ajout d’un tableau séparé « Détails des sorties gratuites » (avec le nom du matériel).
|
|
5
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é.
|
package/pkg/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/pkg/package/package.json
CHANGED
package/pkg/package/plugin.json
CHANGED
|
@@ -1171,6 +1171,9 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1171
1171
|
const accSummary = document.querySelector('#onekite-acc-summary tbody');
|
|
1172
1172
|
const accRows = document.querySelector('#onekite-acc-rows tbody');
|
|
1173
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');
|
|
1174
1177
|
|
|
1175
1178
|
function ymd(d) {
|
|
1176
1179
|
const yyyy = d.getUTCFullYear();
|
|
@@ -1190,10 +1193,17 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1190
1193
|
if (accSummary) accSummary.innerHTML = '';
|
|
1191
1194
|
if (accRows) accRows.innerHTML = '';
|
|
1192
1195
|
if (accFreeRows) accFreeRows.innerHTML = '';
|
|
1196
|
+
if (accGrandTotal) accGrandTotal.textContent = '0.00';
|
|
1197
|
+
if (accPaidCount) accPaidCount.textContent = '0';
|
|
1198
|
+
if (accFreeCount) accFreeCount.textContent = '0';
|
|
1193
1199
|
if (!payload || !payload.ok) {
|
|
1194
1200
|
return;
|
|
1195
1201
|
}
|
|
1196
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
|
+
|
|
1197
1207
|
(payload.summary || []).forEach((s) => {
|
|
1198
1208
|
const tr = document.createElement('tr');
|
|
1199
1209
|
if (s.isFree) {
|
|
@@ -61,6 +61,28 @@ 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 var(--bs-border-color, rgba(0,0,0,.18)); 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:var(--bs-border-color, rgba(0,0,0,.28)); font-weight:700; background:var(--bs-primary-bg-subtle, var(--bs-secondary-bg, var(--bs-tertiary-bg, rgba(0,0,0,.06)))); }
|
|
76
|
+
.onekite-range-day.is-inrange { background:var(--bs-secondary-bg, var(--bs-tertiary-bg, rgba(0,0,0,.04))); }
|
|
77
|
+
.onekite-range-day:focus { outline: none; }
|
|
78
|
+
|
|
79
|
+
@media (prefers-color-scheme: dark) {
|
|
80
|
+
.onekite-range-day { border-color: var(--bs-border-color, rgba(255,255,255,.18)); }
|
|
81
|
+
.onekite-range-day.is-inrange { background: rgba(255,255,255,.08); }
|
|
82
|
+
.onekite-range-day.is-start, .onekite-range-day.is-end { background: rgba(255,255,255,.14); }
|
|
83
|
+
}
|
|
84
|
+
.onekite-range-summary { margin-top:10px; font-size:.9rem; }
|
|
85
|
+
|
|
64
86
|
`;
|
|
65
87
|
document.head.appendChild(style);
|
|
66
88
|
} catch (e) {
|
|
@@ -2030,131 +2052,235 @@ function toDatetimeLocalValue(date) {
|
|
|
2030
2052
|
return d;
|
|
2031
2053
|
}
|
|
2032
2054
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2055
|
+
|
|
2056
|
+
async function openFabDatePicker() {
|
|
2057
|
+
if (!lockAction('fab-date-picker', 700)) return;
|
|
2058
|
+
|
|
2059
|
+
// Cannot book today or past (mobile FAB).
|
|
2060
|
+
const today = new Date();
|
|
2061
|
+
today.setHours(0, 0, 0, 0);
|
|
2062
|
+
const minStart = new Date(today);
|
|
2063
|
+
minStart.setDate(minStart.getDate() + 1);
|
|
2064
|
+
|
|
2065
|
+
// Month cursor for the single-calendar range picker
|
|
2066
|
+
const cursor = new Date(minStart);
|
|
2067
|
+
cursor.setDate(1);
|
|
2068
|
+
|
|
2069
|
+
const state = {
|
|
2070
|
+
cursor,
|
|
2071
|
+
minStart,
|
|
2072
|
+
start: null,
|
|
2073
|
+
end: null, // inclusive
|
|
2074
|
+
};
|
|
2041
2075
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
<div class="mb-2">
|
|
2049
|
-
<label class="form-label">Date de fin (incluse)</label>
|
|
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>
|
|
2052
|
-
</div>
|
|
2076
|
+
const html = `
|
|
2077
|
+
<div class="onekite-range-picker" id="onekite-range-picker">
|
|
2078
|
+
<div class="onekite-range-header">
|
|
2079
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-prev" aria-label="Mois précédent">‹</button>
|
|
2080
|
+
<div class="onekite-range-month" id="onekite-range-month"></div>
|
|
2081
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-next" aria-label="Mois suivant">›</button>
|
|
2053
2082
|
</div>
|
|
2054
|
-
|
|
2083
|
+
<div class="onekite-range-weekdays">
|
|
2084
|
+
<div>L</div><div>M</div><div>M</div><div>J</div><div>V</div><div>S</div><div>D</div>
|
|
2085
|
+
</div>
|
|
2086
|
+
<div class="onekite-range-grid" id="onekite-range-grid"></div>
|
|
2087
|
+
<div class="onekite-range-summary" id="onekite-range-summary"></div>
|
|
2088
|
+
<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>
|
|
2089
|
+
</div>
|
|
2090
|
+
`;
|
|
2091
|
+
|
|
2092
|
+
const dlg = bootbox.dialog({
|
|
2093
|
+
title: 'Choisir une période',
|
|
2094
|
+
message: html,
|
|
2095
|
+
buttons: {
|
|
2096
|
+
cancel: { label: 'Annuler', className: 'btn-secondary' },
|
|
2097
|
+
ok: {
|
|
2098
|
+
label: 'Continuer',
|
|
2099
|
+
className: 'btn-primary',
|
|
2100
|
+
callback: function () {
|
|
2101
|
+
const s = state.start;
|
|
2102
|
+
const e = state.end || state.start;
|
|
2103
|
+
if (!s) {
|
|
2104
|
+
alerts.error('Choisis une date de début.');
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
if (!e) {
|
|
2108
|
+
alerts.error('Choisis une date de fin.');
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
// Convert end inclusive -> end exclusive (FullCalendar rule)
|
|
2112
|
+
const endExcl = new Date(e);
|
|
2113
|
+
endExcl.setDate(endExcl.getDate() + 1);
|
|
2055
2114
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2115
|
+
(async () => {
|
|
2116
|
+
try {
|
|
2117
|
+
if (isDialogOpen) return;
|
|
2118
|
+
isDialogOpen = true;
|
|
2119
|
+
const items = cachedItems || (await loadItems());
|
|
2120
|
+
const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
|
|
2121
|
+
if (chosen && chosen.itemIds && chosen.itemIds.length) {
|
|
2122
|
+
const startDate = toLocalYmd(s);
|
|
2123
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(endExcl);
|
|
2124
|
+
const resp = await requestReservation({
|
|
2125
|
+
start: startDate,
|
|
2126
|
+
end: endDate,
|
|
2127
|
+
itemIds: chosen.itemIds,
|
|
2128
|
+
itemNames: chosen.itemNames,
|
|
2129
|
+
total: chosen.total,
|
|
2130
|
+
});
|
|
2131
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
2132
|
+
showAlert('success', 'Réservation confirmée.');
|
|
2133
|
+
} else {
|
|
2134
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
2135
|
+
}
|
|
2136
|
+
invalidateEventsCache();
|
|
2137
|
+
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2138
|
+
}
|
|
2139
|
+
} catch (err) {
|
|
2140
|
+
// ignore
|
|
2141
|
+
} finally {
|
|
2142
|
+
isDialogOpen = false;
|
|
2072
2143
|
}
|
|
2144
|
+
})();
|
|
2073
2145
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
if (e < s) {
|
|
2080
|
-
alerts.error('La date de fin doit être après la date de début.');
|
|
2081
|
-
return false;
|
|
2082
|
-
}
|
|
2146
|
+
return true;
|
|
2147
|
+
},
|
|
2148
|
+
},
|
|
2149
|
+
},
|
|
2150
|
+
});
|
|
2083
2151
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2152
|
+
function sameDay(a, b) {
|
|
2153
|
+
return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
2154
|
+
}
|
|
2087
2155
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2156
|
+
function clampToMidnight(d) {
|
|
2157
|
+
const x = new Date(d);
|
|
2158
|
+
x.setHours(0, 0, 0, 0);
|
|
2159
|
+
return x;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function monthLabel(d) {
|
|
2163
|
+
try {
|
|
2164
|
+
return d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
|
|
2165
|
+
} catch (e) {
|
|
2166
|
+
return `${d.getMonth() + 1}/${d.getFullYear()}`;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
function render() {
|
|
2171
|
+
const monthEl = document.getElementById('onekite-range-month');
|
|
2172
|
+
const gridEl = document.getElementById('onekite-range-grid');
|
|
2173
|
+
const sumEl = document.getElementById('onekite-range-summary');
|
|
2174
|
+
if (!monthEl || !gridEl || !sumEl) return;
|
|
2175
|
+
|
|
2176
|
+
monthEl.textContent = monthLabel(state.cursor);
|
|
2177
|
+
gridEl.innerHTML = '';
|
|
2178
|
+
|
|
2179
|
+
const year = state.cursor.getFullYear();
|
|
2180
|
+
const month = state.cursor.getMonth();
|
|
2181
|
+
const first = new Date(year, month, 1);
|
|
2182
|
+
const last = new Date(year, month + 1, 0);
|
|
2183
|
+
|
|
2184
|
+
// Monday-based week index: 0..6
|
|
2185
|
+
const firstDow = (first.getDay() + 6) % 7;
|
|
2186
|
+
const daysInMonth = last.getDate();
|
|
2187
|
+
|
|
2188
|
+
// Empty cells before month start
|
|
2189
|
+
for (let i = 0; i < firstDow; i++) {
|
|
2190
|
+
const div = document.createElement('div');
|
|
2191
|
+
div.className = 'onekite-range-day is-empty';
|
|
2192
|
+
gridEl.appendChild(div);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const s = state.start;
|
|
2196
|
+
const e = state.end || state.start;
|
|
2197
|
+
|
|
2198
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
2199
|
+
const d = new Date(year, month, day);
|
|
2200
|
+
d.setHours(0, 0, 0, 0);
|
|
2201
|
+
|
|
2202
|
+
const div = document.createElement('div');
|
|
2203
|
+
div.className = 'onekite-range-day';
|
|
2204
|
+
div.textContent = String(day);
|
|
2205
|
+
|
|
2206
|
+
const disabled = d < state.minStart;
|
|
2207
|
+
if (disabled) div.classList.add('is-disabled');
|
|
2208
|
+
|
|
2209
|
+
if (s && sameDay(d, s)) div.classList.add('is-start');
|
|
2210
|
+
if (state.end && sameDay(d, state.end)) div.classList.add('is-end');
|
|
2211
|
+
|
|
2212
|
+
// Highlight the full selected range (inclusive), including start/end.
|
|
2213
|
+
if (s && e) {
|
|
2214
|
+
const t = d.getTime();
|
|
2215
|
+
const ts = s.getTime();
|
|
2216
|
+
const te = e.getTime();
|
|
2217
|
+
if (!state.end) {
|
|
2218
|
+
if (t === ts) div.classList.add('is-inrange');
|
|
2107
2219
|
} else {
|
|
2108
|
-
|
|
2220
|
+
if (t >= ts && t <= te) div.classList.add('is-inrange');
|
|
2109
2221
|
}
|
|
2110
|
-
|
|
2111
|
-
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2112
|
-
}
|
|
2113
|
-
} catch (err) {
|
|
2114
|
-
// ignore
|
|
2115
|
-
} finally {
|
|
2116
|
-
isDialogOpen = false;
|
|
2117
|
-
}
|
|
2118
|
-
})();
|
|
2222
|
+
}
|
|
2119
2223
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2224
|
+
if (!disabled) {
|
|
2225
|
+
div.addEventListener('click', () => {
|
|
2226
|
+
const clicked = clampToMidnight(d);
|
|
2227
|
+
// First click: set start
|
|
2228
|
+
if (!state.start || (state.start && state.end)) {
|
|
2229
|
+
state.start = clicked;
|
|
2230
|
+
state.end = null;
|
|
2231
|
+
// If cursor month is before selected month, keep cursor
|
|
2232
|
+
render();
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
// Second click: set end (inclusive)
|
|
2236
|
+
if (clicked < state.start) {
|
|
2237
|
+
state.start = clicked;
|
|
2238
|
+
state.end = null;
|
|
2239
|
+
render();
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
state.end = clicked;
|
|
2243
|
+
render();
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2126
2246
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
-
}
|
|
2152
|
-
if (startEl) startEl.focus();
|
|
2153
|
-
} catch (e) {}
|
|
2154
|
-
});
|
|
2247
|
+
gridEl.appendChild(div);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// Summary
|
|
2251
|
+
if (!state.start) {
|
|
2252
|
+
sumEl.textContent = 'Aucune date sélectionnée.';
|
|
2253
|
+
} else {
|
|
2254
|
+
const startTxt = formatDdMmYyyy(state.start);
|
|
2255
|
+
const endTxt = formatDdMmYyyy(state.end || state.start);
|
|
2256
|
+
sumEl.textContent = (state.end && !sameDay(state.start, state.end))
|
|
2257
|
+
? `Du ${startTxt} au ${endTxt}`
|
|
2258
|
+
: `Le ${startTxt}`;
|
|
2259
|
+
}
|
|
2155
2260
|
}
|
|
2156
2261
|
|
|
2157
|
-
|
|
2262
|
+
dlg.on('shown.bs.modal', () => {
|
|
2263
|
+
try {
|
|
2264
|
+
const prevBtn = document.getElementById('onekite-range-prev');
|
|
2265
|
+
const nextBtn = document.getElementById('onekite-range-next');
|
|
2266
|
+
if (prevBtn) {
|
|
2267
|
+
prevBtn.addEventListener('click', () => {
|
|
2268
|
+
state.cursor.setMonth(state.cursor.getMonth() - 1);
|
|
2269
|
+
render();
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
if (nextBtn) {
|
|
2273
|
+
nextBtn.addEventListener('click', () => {
|
|
2274
|
+
state.cursor.setMonth(state.cursor.getMonth() + 1);
|
|
2275
|
+
render();
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
render();
|
|
2279
|
+
} catch (e) {}
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function parseYmdDate(ymdStr) {
|
|
2158
2284
|
// Expect YYYY-MM-DD (from <input type="date">)
|
|
2159
2285
|
if (!ymdStr || typeof ymdStr !== 'string') return null;
|
|
2160
2286
|
const m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(ymdStr);
|
|
@@ -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">
|
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -1171,6 +1171,9 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1171
1171
|
const accSummary = document.querySelector('#onekite-acc-summary tbody');
|
|
1172
1172
|
const accRows = document.querySelector('#onekite-acc-rows tbody');
|
|
1173
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');
|
|
1174
1177
|
|
|
1175
1178
|
function ymd(d) {
|
|
1176
1179
|
const yyyy = d.getUTCFullYear();
|
|
@@ -1190,10 +1193,17 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1190
1193
|
if (accSummary) accSummary.innerHTML = '';
|
|
1191
1194
|
if (accRows) accRows.innerHTML = '';
|
|
1192
1195
|
if (accFreeRows) accFreeRows.innerHTML = '';
|
|
1196
|
+
if (accGrandTotal) accGrandTotal.textContent = '0.00';
|
|
1197
|
+
if (accPaidCount) accPaidCount.textContent = '0';
|
|
1198
|
+
if (accFreeCount) accFreeCount.textContent = '0';
|
|
1193
1199
|
if (!payload || !payload.ok) {
|
|
1194
1200
|
return;
|
|
1195
1201
|
}
|
|
1196
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
|
+
|
|
1197
1207
|
(payload.summary || []).forEach((s) => {
|
|
1198
1208
|
const tr = document.createElement('tr');
|
|
1199
1209
|
if (s.isFree) {
|
package/public/client.js
CHANGED
|
@@ -61,6 +61,28 @@ 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 var(--bs-border-color, rgba(0,0,0,.18)); 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:var(--bs-border-color, rgba(0,0,0,.28)); font-weight:700; background:var(--bs-primary-bg-subtle, var(--bs-secondary-bg, var(--bs-tertiary-bg, rgba(0,0,0,.06)))); }
|
|
76
|
+
.onekite-range-day.is-inrange { background:var(--bs-secondary-bg, var(--bs-tertiary-bg, rgba(0,0,0,.04))); }
|
|
77
|
+
.onekite-range-day:focus { outline: none; }
|
|
78
|
+
|
|
79
|
+
@media (prefers-color-scheme: dark) {
|
|
80
|
+
.onekite-range-day { border-color: var(--bs-border-color, rgba(255,255,255,.18)); }
|
|
81
|
+
.onekite-range-day.is-inrange { background: rgba(255,255,255,.08); }
|
|
82
|
+
.onekite-range-day.is-start, .onekite-range-day.is-end { background: rgba(255,255,255,.14); }
|
|
83
|
+
}
|
|
84
|
+
.onekite-range-summary { margin-top:10px; font-size:.9rem; }
|
|
85
|
+
|
|
64
86
|
`;
|
|
65
87
|
document.head.appendChild(style);
|
|
66
88
|
} catch (e) {
|
|
@@ -2030,131 +2052,235 @@ function toDatetimeLocalValue(date) {
|
|
|
2030
2052
|
return d;
|
|
2031
2053
|
}
|
|
2032
2054
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2055
|
+
|
|
2056
|
+
async function openFabDatePicker() {
|
|
2057
|
+
if (!lockAction('fab-date-picker', 700)) return;
|
|
2058
|
+
|
|
2059
|
+
// Cannot book today or past (mobile FAB).
|
|
2060
|
+
const today = new Date();
|
|
2061
|
+
today.setHours(0, 0, 0, 0);
|
|
2062
|
+
const minStart = new Date(today);
|
|
2063
|
+
minStart.setDate(minStart.getDate() + 1);
|
|
2064
|
+
|
|
2065
|
+
// Month cursor for the single-calendar range picker
|
|
2066
|
+
const cursor = new Date(minStart);
|
|
2067
|
+
cursor.setDate(1);
|
|
2068
|
+
|
|
2069
|
+
const state = {
|
|
2070
|
+
cursor,
|
|
2071
|
+
minStart,
|
|
2072
|
+
start: null,
|
|
2073
|
+
end: null, // inclusive
|
|
2074
|
+
};
|
|
2041
2075
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
<div class="mb-2">
|
|
2049
|
-
<label class="form-label">Date de fin (incluse)</label>
|
|
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>
|
|
2052
|
-
</div>
|
|
2076
|
+
const html = `
|
|
2077
|
+
<div class="onekite-range-picker" id="onekite-range-picker">
|
|
2078
|
+
<div class="onekite-range-header">
|
|
2079
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-prev" aria-label="Mois précédent">‹</button>
|
|
2080
|
+
<div class="onekite-range-month" id="onekite-range-month"></div>
|
|
2081
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-next" aria-label="Mois suivant">›</button>
|
|
2053
2082
|
</div>
|
|
2054
|
-
|
|
2083
|
+
<div class="onekite-range-weekdays">
|
|
2084
|
+
<div>L</div><div>M</div><div>M</div><div>J</div><div>V</div><div>S</div><div>D</div>
|
|
2085
|
+
</div>
|
|
2086
|
+
<div class="onekite-range-grid" id="onekite-range-grid"></div>
|
|
2087
|
+
<div class="onekite-range-summary" id="onekite-range-summary"></div>
|
|
2088
|
+
<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>
|
|
2089
|
+
</div>
|
|
2090
|
+
`;
|
|
2091
|
+
|
|
2092
|
+
const dlg = bootbox.dialog({
|
|
2093
|
+
title: 'Choisir une période',
|
|
2094
|
+
message: html,
|
|
2095
|
+
buttons: {
|
|
2096
|
+
cancel: { label: 'Annuler', className: 'btn-secondary' },
|
|
2097
|
+
ok: {
|
|
2098
|
+
label: 'Continuer',
|
|
2099
|
+
className: 'btn-primary',
|
|
2100
|
+
callback: function () {
|
|
2101
|
+
const s = state.start;
|
|
2102
|
+
const e = state.end || state.start;
|
|
2103
|
+
if (!s) {
|
|
2104
|
+
alerts.error('Choisis une date de début.');
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
if (!e) {
|
|
2108
|
+
alerts.error('Choisis une date de fin.');
|
|
2109
|
+
return false;
|
|
2110
|
+
}
|
|
2111
|
+
// Convert end inclusive -> end exclusive (FullCalendar rule)
|
|
2112
|
+
const endExcl = new Date(e);
|
|
2113
|
+
endExcl.setDate(endExcl.getDate() + 1);
|
|
2055
2114
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2115
|
+
(async () => {
|
|
2116
|
+
try {
|
|
2117
|
+
if (isDialogOpen) return;
|
|
2118
|
+
isDialogOpen = true;
|
|
2119
|
+
const items = cachedItems || (await loadItems());
|
|
2120
|
+
const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
|
|
2121
|
+
if (chosen && chosen.itemIds && chosen.itemIds.length) {
|
|
2122
|
+
const startDate = toLocalYmd(s);
|
|
2123
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(endExcl);
|
|
2124
|
+
const resp = await requestReservation({
|
|
2125
|
+
start: startDate,
|
|
2126
|
+
end: endDate,
|
|
2127
|
+
itemIds: chosen.itemIds,
|
|
2128
|
+
itemNames: chosen.itemNames,
|
|
2129
|
+
total: chosen.total,
|
|
2130
|
+
});
|
|
2131
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
2132
|
+
showAlert('success', 'Réservation confirmée.');
|
|
2133
|
+
} else {
|
|
2134
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
2135
|
+
}
|
|
2136
|
+
invalidateEventsCache();
|
|
2137
|
+
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2138
|
+
}
|
|
2139
|
+
} catch (err) {
|
|
2140
|
+
// ignore
|
|
2141
|
+
} finally {
|
|
2142
|
+
isDialogOpen = false;
|
|
2072
2143
|
}
|
|
2144
|
+
})();
|
|
2073
2145
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
if (e < s) {
|
|
2080
|
-
alerts.error('La date de fin doit être après la date de début.');
|
|
2081
|
-
return false;
|
|
2082
|
-
}
|
|
2146
|
+
return true;
|
|
2147
|
+
},
|
|
2148
|
+
},
|
|
2149
|
+
},
|
|
2150
|
+
});
|
|
2083
2151
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2152
|
+
function sameDay(a, b) {
|
|
2153
|
+
return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
2154
|
+
}
|
|
2087
2155
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2156
|
+
function clampToMidnight(d) {
|
|
2157
|
+
const x = new Date(d);
|
|
2158
|
+
x.setHours(0, 0, 0, 0);
|
|
2159
|
+
return x;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function monthLabel(d) {
|
|
2163
|
+
try {
|
|
2164
|
+
return d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
|
|
2165
|
+
} catch (e) {
|
|
2166
|
+
return `${d.getMonth() + 1}/${d.getFullYear()}`;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
function render() {
|
|
2171
|
+
const monthEl = document.getElementById('onekite-range-month');
|
|
2172
|
+
const gridEl = document.getElementById('onekite-range-grid');
|
|
2173
|
+
const sumEl = document.getElementById('onekite-range-summary');
|
|
2174
|
+
if (!monthEl || !gridEl || !sumEl) return;
|
|
2175
|
+
|
|
2176
|
+
monthEl.textContent = monthLabel(state.cursor);
|
|
2177
|
+
gridEl.innerHTML = '';
|
|
2178
|
+
|
|
2179
|
+
const year = state.cursor.getFullYear();
|
|
2180
|
+
const month = state.cursor.getMonth();
|
|
2181
|
+
const first = new Date(year, month, 1);
|
|
2182
|
+
const last = new Date(year, month + 1, 0);
|
|
2183
|
+
|
|
2184
|
+
// Monday-based week index: 0..6
|
|
2185
|
+
const firstDow = (first.getDay() + 6) % 7;
|
|
2186
|
+
const daysInMonth = last.getDate();
|
|
2187
|
+
|
|
2188
|
+
// Empty cells before month start
|
|
2189
|
+
for (let i = 0; i < firstDow; i++) {
|
|
2190
|
+
const div = document.createElement('div');
|
|
2191
|
+
div.className = 'onekite-range-day is-empty';
|
|
2192
|
+
gridEl.appendChild(div);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const s = state.start;
|
|
2196
|
+
const e = state.end || state.start;
|
|
2197
|
+
|
|
2198
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
2199
|
+
const d = new Date(year, month, day);
|
|
2200
|
+
d.setHours(0, 0, 0, 0);
|
|
2201
|
+
|
|
2202
|
+
const div = document.createElement('div');
|
|
2203
|
+
div.className = 'onekite-range-day';
|
|
2204
|
+
div.textContent = String(day);
|
|
2205
|
+
|
|
2206
|
+
const disabled = d < state.minStart;
|
|
2207
|
+
if (disabled) div.classList.add('is-disabled');
|
|
2208
|
+
|
|
2209
|
+
if (s && sameDay(d, s)) div.classList.add('is-start');
|
|
2210
|
+
if (state.end && sameDay(d, state.end)) div.classList.add('is-end');
|
|
2211
|
+
|
|
2212
|
+
// Highlight the full selected range (inclusive), including start/end.
|
|
2213
|
+
if (s && e) {
|
|
2214
|
+
const t = d.getTime();
|
|
2215
|
+
const ts = s.getTime();
|
|
2216
|
+
const te = e.getTime();
|
|
2217
|
+
if (!state.end) {
|
|
2218
|
+
if (t === ts) div.classList.add('is-inrange');
|
|
2107
2219
|
} else {
|
|
2108
|
-
|
|
2220
|
+
if (t >= ts && t <= te) div.classList.add('is-inrange');
|
|
2109
2221
|
}
|
|
2110
|
-
|
|
2111
|
-
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2112
|
-
}
|
|
2113
|
-
} catch (err) {
|
|
2114
|
-
// ignore
|
|
2115
|
-
} finally {
|
|
2116
|
-
isDialogOpen = false;
|
|
2117
|
-
}
|
|
2118
|
-
})();
|
|
2222
|
+
}
|
|
2119
2223
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2224
|
+
if (!disabled) {
|
|
2225
|
+
div.addEventListener('click', () => {
|
|
2226
|
+
const clicked = clampToMidnight(d);
|
|
2227
|
+
// First click: set start
|
|
2228
|
+
if (!state.start || (state.start && state.end)) {
|
|
2229
|
+
state.start = clicked;
|
|
2230
|
+
state.end = null;
|
|
2231
|
+
// If cursor month is before selected month, keep cursor
|
|
2232
|
+
render();
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
// Second click: set end (inclusive)
|
|
2236
|
+
if (clicked < state.start) {
|
|
2237
|
+
state.start = clicked;
|
|
2238
|
+
state.end = null;
|
|
2239
|
+
render();
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
state.end = clicked;
|
|
2243
|
+
render();
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2126
2246
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
-
}
|
|
2152
|
-
if (startEl) startEl.focus();
|
|
2153
|
-
} catch (e) {}
|
|
2154
|
-
});
|
|
2247
|
+
gridEl.appendChild(div);
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// Summary
|
|
2251
|
+
if (!state.start) {
|
|
2252
|
+
sumEl.textContent = 'Aucune date sélectionnée.';
|
|
2253
|
+
} else {
|
|
2254
|
+
const startTxt = formatDdMmYyyy(state.start);
|
|
2255
|
+
const endTxt = formatDdMmYyyy(state.end || state.start);
|
|
2256
|
+
sumEl.textContent = (state.end && !sameDay(state.start, state.end))
|
|
2257
|
+
? `Du ${startTxt} au ${endTxt}`
|
|
2258
|
+
: `Le ${startTxt}`;
|
|
2259
|
+
}
|
|
2155
2260
|
}
|
|
2156
2261
|
|
|
2157
|
-
|
|
2262
|
+
dlg.on('shown.bs.modal', () => {
|
|
2263
|
+
try {
|
|
2264
|
+
const prevBtn = document.getElementById('onekite-range-prev');
|
|
2265
|
+
const nextBtn = document.getElementById('onekite-range-next');
|
|
2266
|
+
if (prevBtn) {
|
|
2267
|
+
prevBtn.addEventListener('click', () => {
|
|
2268
|
+
state.cursor.setMonth(state.cursor.getMonth() - 1);
|
|
2269
|
+
render();
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
if (nextBtn) {
|
|
2273
|
+
nextBtn.addEventListener('click', () => {
|
|
2274
|
+
state.cursor.setMonth(state.cursor.getMonth() + 1);
|
|
2275
|
+
render();
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
render();
|
|
2279
|
+
} catch (e) {}
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function parseYmdDate(ymdStr) {
|
|
2158
2284
|
// Expect YYYY-MM-DD (from <input type="date">)
|
|
2159
2285
|
if (!ymdStr || typeof ymdStr !== 'string') return null;
|
|
2160
2286
|
const m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(ymdStr);
|
|
@@ -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">
|