nodebb-plugin-onekite-calendar 2.0.11 → 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 +29 -0
- package/lib/admin.js +21 -9
- package/lib/api.js +235 -4
- package/lib/db.js +114 -0
- package/lib/helloassoWebhook.js +28 -0
- package/library.js +7 -0
- package/package.json +1 -1
- package/pkg/package/CHANGELOG.md +106 -0
- package/pkg/package/lib/admin.js +554 -0
- package/pkg/package/lib/api.js +1458 -0
- package/pkg/package/lib/controllers.js +11 -0
- package/pkg/package/lib/db.js +224 -0
- package/pkg/package/lib/discord.js +190 -0
- package/pkg/package/lib/helloasso.js +352 -0
- package/pkg/package/lib/helloassoWebhook.js +389 -0
- package/pkg/package/lib/scheduler.js +201 -0
- package/pkg/package/lib/widgets.js +460 -0
- package/pkg/package/library.js +164 -0
- package/pkg/package/package.json +14 -0
- package/pkg/package/plugin.json +43 -0
- package/pkg/package/public/admin.js +1477 -0
- package/pkg/package/public/client.js +2228 -0
- package/pkg/package/templates/admin/plugins/calendar-onekite.tpl +298 -0
- package/pkg/package/templates/calendar-onekite.tpl +51 -0
- package/pkg/package/templates/emails/calendar-onekite_approved.tpl +40 -0
- package/pkg/package/templates/emails/calendar-onekite_cancelled.tpl +15 -0
- package/pkg/package/templates/emails/calendar-onekite_expired.tpl +11 -0
- package/pkg/package/templates/emails/calendar-onekite_paid.tpl +15 -0
- package/pkg/package/templates/emails/calendar-onekite_pending.tpl +15 -0
- package/pkg/package/templates/emails/calendar-onekite_refused.tpl +15 -0
- package/pkg/package/templates/emails/calendar-onekite_reminder.tpl +20 -0
- package/plugin.json +1 -1
- package/public/admin.js +205 -4
- package/public/client.js +238 -7
- package/templates/admin/plugins/calendar-onekite.tpl +74 -0
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,22 +1189,33 @@ 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
|
}
|
|
1194
1196
|
|
|
1195
1197
|
(payload.summary || []).forEach((s) => {
|
|
1196
1198
|
const tr = document.createElement('tr');
|
|
1197
|
-
|
|
1199
|
+
if (s.isFree) {
|
|
1200
|
+
tr.innerHTML = `<td><em>${escapeHtml(s.item)}</em></td><td>${escapeHtml(String(s.count || 0))}</td><td>-</td>`;
|
|
1201
|
+
} else {
|
|
1202
|
+
tr.innerHTML = `<td>${escapeHtml(s.item)}</td><td>${escapeHtml(String(s.count || 0))}</td><td>${escapeHtml((Number(s.total) || 0).toFixed(2))}</td>`;
|
|
1203
|
+
}
|
|
1198
1204
|
accSummary && accSummary.appendChild(tr);
|
|
1199
1205
|
});
|
|
1200
1206
|
|
|
1201
1207
|
(payload.rows || []).forEach((r) => {
|
|
1202
1208
|
const tr = document.createElement('tr');
|
|
1203
|
-
const user = r.username ? `<a href="/user/${encodeURIComponent(r.username)}" target="_blank">${escapeHtml(r.username)}</a
|
|
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>' : '');
|
|
1204
1210
|
const items = Array.isArray(r.items) ? r.items.map((x) => escapeHtml(x)).join('<br>') : '';
|
|
1205
|
-
|
|
1206
|
-
|
|
1211
|
+
const totalCell = r.isFree ? '-' : escapeHtml((Number(r.total) || 0).toFixed(2));
|
|
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
|
+
}
|
|
1207
1219
|
});
|
|
1208
1220
|
}
|
|
1209
1221
|
|
|
@@ -1270,6 +1282,195 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
1270
1282
|
}
|
|
1271
1283
|
|
|
1272
1284
|
}
|
|
1285
|
+
|
|
1286
|
+
// --------------------
|
|
1287
|
+
// Maintenance (simple ON/OFF)
|
|
1288
|
+
// --------------------
|
|
1289
|
+
const maintSearch = document.getElementById('onekite-maint-search');
|
|
1290
|
+
const maintRefresh = document.getElementById('onekite-maint-refresh');
|
|
1291
|
+
const maintAllOn = document.getElementById('onekite-maint-all-on');
|
|
1292
|
+
const maintAllOff = document.getElementById('onekite-maint-all-off');
|
|
1293
|
+
const maintTableBody = document.querySelector('#onekite-maint-table tbody');
|
|
1294
|
+
|
|
1295
|
+
let maintItemsCache = [];
|
|
1296
|
+
async function loadItemsWithMaintenance() {
|
|
1297
|
+
// Public items endpoint returns catalog + maintenance flag
|
|
1298
|
+
return await fetchJson('/api/v3/plugins/calendar-onekite/items');
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function renderMaintenanceTable(items) {
|
|
1302
|
+
if (!maintTableBody) return;
|
|
1303
|
+
const q = maintSearch ? String(maintSearch.value || '').trim().toLowerCase() : '';
|
|
1304
|
+
const filtered = (items || []).filter((it) => {
|
|
1305
|
+
if (!it) return false;
|
|
1306
|
+
if (!q) return true;
|
|
1307
|
+
return String(it.name || '').toLowerCase().includes(q);
|
|
1308
|
+
});
|
|
1309
|
+
maintTableBody.innerHTML = filtered.map((it) => {
|
|
1310
|
+
const id = String(it.id);
|
|
1311
|
+
const name = String(it.name || '').replace(/</g, '<').replace(/>/g, '>');
|
|
1312
|
+
const checked = it.maintenance ? 'checked' : '';
|
|
1313
|
+
return `<tr data-itemid="${id}">
|
|
1314
|
+
<td>${name}</td>
|
|
1315
|
+
<td>
|
|
1316
|
+
<div class="form-check form-switch">
|
|
1317
|
+
<input class="form-check-input onekite-maint-toggle" type="checkbox" ${checked} />
|
|
1318
|
+
</div>
|
|
1319
|
+
</td>
|
|
1320
|
+
</tr>`;
|
|
1321
|
+
}).join('') || '<tr><td colspan="2" class="text-muted">Aucun matériel.</td></tr>';
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function refreshMaintenance() {
|
|
1325
|
+
try {
|
|
1326
|
+
const items = await loadItemsWithMaintenance();
|
|
1327
|
+
maintItemsCache = Array.isArray(items) ? items : [];
|
|
1328
|
+
renderMaintenanceTable(maintItemsCache);
|
|
1329
|
+
} catch (e) {
|
|
1330
|
+
showAlert('error', 'Impossible de charger le matériel (maintenance).');
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (maintRefresh) maintRefresh.addEventListener('click', refreshMaintenance);
|
|
1335
|
+
|
|
1336
|
+
async function setAllMaintenance(enabled) {
|
|
1337
|
+
const label = enabled ? 'mettre TOUS les matériels en maintenance' : 'enlever la maintenance sur TOUS les matériels';
|
|
1338
|
+
const ok = window.confirm(`Confirmer : ${label} ?`);
|
|
1339
|
+
if (!ok) return;
|
|
1340
|
+
try {
|
|
1341
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/maintenance', {
|
|
1342
|
+
method: 'PUT',
|
|
1343
|
+
body: JSON.stringify({ enabled: !!enabled, all: true }),
|
|
1344
|
+
});
|
|
1345
|
+
// Update cache locally without refetching if possible
|
|
1346
|
+
maintItemsCache = maintItemsCache.map((it) => Object.assign({}, it, { maintenance: !!enabled }));
|
|
1347
|
+
renderMaintenanceTable(maintItemsCache);
|
|
1348
|
+
showAlert('success', enabled ? 'Tous les matériels sont en maintenance.' : 'Maintenance supprimée pour tous les matériels.');
|
|
1349
|
+
} catch (e) {
|
|
1350
|
+
showAlert('error', 'Impossible de modifier la maintenance pour tous les matériels.');
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (maintAllOn) maintAllOn.addEventListener('click', () => setAllMaintenance(true));
|
|
1355
|
+
if (maintAllOff) maintAllOff.addEventListener('click', () => setAllMaintenance(false));
|
|
1356
|
+
if (maintSearch) maintSearch.addEventListener('input', () => renderMaintenanceTable(maintItemsCache));
|
|
1357
|
+
if (maintTableBody) {
|
|
1358
|
+
maintTableBody.addEventListener('change', async (ev) => {
|
|
1359
|
+
const el = ev && ev.target;
|
|
1360
|
+
if (!el || !el.classList || !el.classList.contains('onekite-maint-toggle')) return;
|
|
1361
|
+
const tr = el.closest('tr');
|
|
1362
|
+
const itemId = tr ? tr.getAttribute('data-itemid') : '';
|
|
1363
|
+
const enabled = !!el.checked;
|
|
1364
|
+
if (!itemId) return;
|
|
1365
|
+
// Optimistic UI
|
|
1366
|
+
try {
|
|
1367
|
+
await fetchJson(`/api/v3/plugins/calendar-onekite/maintenance/${encodeURIComponent(String(itemId))}`, {
|
|
1368
|
+
method: 'PUT',
|
|
1369
|
+
body: JSON.stringify({ enabled }),
|
|
1370
|
+
});
|
|
1371
|
+
// Update cache
|
|
1372
|
+
maintItemsCache = maintItemsCache.map((it) => (String(it.id) === String(itemId) ? Object.assign({}, it, { maintenance: enabled }) : it));
|
|
1373
|
+
showAlert('success', enabled ? 'Maintenance activée.' : 'Maintenance désactivée.');
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
// Revert
|
|
1376
|
+
el.checked = !enabled;
|
|
1377
|
+
showAlert('error', 'Impossible de modifier la maintenance.');
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Load once
|
|
1383
|
+
if (maintTableBody) {
|
|
1384
|
+
refreshMaintenance();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// --------------------
|
|
1388
|
+
// Audit (purge par année)
|
|
1389
|
+
// --------------------
|
|
1390
|
+
const auditYearEl = document.getElementById('onekite-audit-year');
|
|
1391
|
+
const auditSearchEl = document.getElementById('onekite-audit-search');
|
|
1392
|
+
const auditRefreshBtn = document.getElementById('onekite-audit-refresh');
|
|
1393
|
+
const auditPurgeBtn = document.getElementById('onekite-audit-purge');
|
|
1394
|
+
const auditTbody = document.querySelector('#onekite-audit-table tbody');
|
|
1395
|
+
|
|
1396
|
+
let auditCache = [];
|
|
1397
|
+
function fmtDateTime(ts) {
|
|
1398
|
+
try {
|
|
1399
|
+
return new Date(Number(ts) || 0).toLocaleString('fr-FR');
|
|
1400
|
+
} catch (e) {
|
|
1401
|
+
return String(ts || '');
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function renderAudit(entries) {
|
|
1405
|
+
if (!auditTbody) return;
|
|
1406
|
+
const q = auditSearchEl ? String(auditSearchEl.value || '').trim().toLowerCase() : '';
|
|
1407
|
+
const rows = (entries || []).filter((e) => {
|
|
1408
|
+
if (!e) return false;
|
|
1409
|
+
if (!q) return true;
|
|
1410
|
+
const hay = [e.action, e.actorUsername, e.targetType, e.targetId, JSON.stringify(e)].join(' ').toLowerCase();
|
|
1411
|
+
return hay.includes(q);
|
|
1412
|
+
}).map((e) => {
|
|
1413
|
+
const actor = e.actorUsername ? `${e.actorUsername} (#${e.actorUid || 0})` : `#${e.actorUid || 0}`;
|
|
1414
|
+
const target = `${e.targetType || ''} ${e.targetId || ''}`.trim();
|
|
1415
|
+
const details = (() => {
|
|
1416
|
+
const parts = [];
|
|
1417
|
+
if (e.itemNames && Array.isArray(e.itemNames) && e.itemNames.length) parts.push(e.itemNames.join(', '));
|
|
1418
|
+
if (e.startDate && e.endDate) parts.push(`${e.startDate} → ${e.endDate}`);
|
|
1419
|
+
if (e.reason) parts.push(`Raison: ${e.reason}`);
|
|
1420
|
+
if (e.removed) parts.push(`Supprimés: ${e.removed}`);
|
|
1421
|
+
return parts.join(' — ');
|
|
1422
|
+
})();
|
|
1423
|
+
return `<tr>
|
|
1424
|
+
<td>${fmtDateTime(e.ts)}</td>
|
|
1425
|
+
<td>${String(actor).replace(/</g,'<').replace(/>/g,'>')}</td>
|
|
1426
|
+
<td>${String(e.action || '').replace(/</g,'<').replace(/>/g,'>')}</td>
|
|
1427
|
+
<td>${String(target).replace(/</g,'<').replace(/>/g,'>')}</td>
|
|
1428
|
+
<td class="text-muted">${String(details || '').replace(/</g,'<').replace(/>/g,'>')}</td>
|
|
1429
|
+
</tr>`;
|
|
1430
|
+
}).join('');
|
|
1431
|
+
auditTbody.innerHTML = rows || '<tr><td colspan="5" class="text-muted">Aucune entrée.</td></tr>';
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function refreshAudit() {
|
|
1435
|
+
if (!auditTbody) return;
|
|
1436
|
+
const y = Number((auditYearEl && auditYearEl.value) || new Date().getFullYear());
|
|
1437
|
+
try {
|
|
1438
|
+
const data = await fetchJson(`/api/v3/plugins/calendar-onekite/audit?year=${encodeURIComponent(String(y))}&limit=300`);
|
|
1439
|
+
auditCache = (data && data.entries) ? data.entries : [];
|
|
1440
|
+
renderAudit(auditCache);
|
|
1441
|
+
} catch (e) {
|
|
1442
|
+
showAlert('error', 'Impossible de charger l\'audit.');
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (auditYearEl && !auditYearEl.value) {
|
|
1447
|
+
auditYearEl.value = String(new Date().getFullYear());
|
|
1448
|
+
}
|
|
1449
|
+
if (auditRefreshBtn) auditRefreshBtn.addEventListener('click', refreshAudit);
|
|
1450
|
+
if (auditSearchEl) auditSearchEl.addEventListener('input', () => renderAudit(auditCache));
|
|
1451
|
+
if (auditPurgeBtn) {
|
|
1452
|
+
auditPurgeBtn.addEventListener('click', async () => {
|
|
1453
|
+
const y = Number((auditYearEl && auditYearEl.value) || 0);
|
|
1454
|
+
if (!y) return;
|
|
1455
|
+
const ok = window.confirm(`Purger définitivement l’audit de l’année ${y} ?`);
|
|
1456
|
+
if (!ok) return;
|
|
1457
|
+
try {
|
|
1458
|
+
const res = await fetchJson('/api/v3/plugins/calendar-onekite/audit/purge', {
|
|
1459
|
+
method: 'POST',
|
|
1460
|
+
body: JSON.stringify({ year: y }),
|
|
1461
|
+
});
|
|
1462
|
+
showAlert('success', `Audit purgé : ${res && res.removed ? res.removed : 0} entrée(s).`);
|
|
1463
|
+
await refreshAudit();
|
|
1464
|
+
} catch (e) {
|
|
1465
|
+
showAlert('error', 'Purge audit impossible.');
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Load once
|
|
1471
|
+
if (auditTbody) {
|
|
1472
|
+
refreshAudit();
|
|
1473
|
+
}
|
|
1273
1474
|
}
|
|
1274
1475
|
|
|
1275
1476
|
return { init };
|
package/public/client.js
CHANGED
|
@@ -41,7 +41,27 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
41
41
|
.fc .onekite-day-disabled:hover {
|
|
42
42
|
cursor: not-allowed;
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
/* Mobile floating action button (FAB) */
|
|
46
|
+
@media (max-width: 768px) {
|
|
47
|
+
.onekite-fab {
|
|
48
|
+
position: fixed;
|
|
49
|
+
right: 16px;
|
|
50
|
+
bottom: 16px;
|
|
51
|
+
width: 56px;
|
|
52
|
+
height: 56px;
|
|
53
|
+
border-radius: 999px;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
z-index: 1055;
|
|
58
|
+
box-shadow: 0 10px 22px rgba(0,0,0,.25);
|
|
59
|
+
}
|
|
60
|
+
.onekite-fab .fa, .onekite-fab .fas, .onekite-fab .fa-solid {
|
|
61
|
+
font-size: 20px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
45
65
|
document.head.appendChild(style);
|
|
46
66
|
} catch (e) {
|
|
47
67
|
// ignore
|
|
@@ -52,6 +72,16 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
52
72
|
// interactions or quick re-renders.
|
|
53
73
|
let isDialogOpen = false;
|
|
54
74
|
|
|
75
|
+
// Cached list of reservable items (loaded once per page init)
|
|
76
|
+
let cachedItems = null;
|
|
77
|
+
|
|
78
|
+
// Current FullCalendar instance (for refresh after actions)
|
|
79
|
+
let currentCalendar = null;
|
|
80
|
+
|
|
81
|
+
// Mobile FAB (mounted only on the calendar page)
|
|
82
|
+
let fabEl = null;
|
|
83
|
+
let fabHandler = null;
|
|
84
|
+
|
|
55
85
|
// Prevent double taps/clicks on actions that trigger network calls (mobile especially).
|
|
56
86
|
const actionLocks = new Map(); // key -> ts
|
|
57
87
|
function lockAction(key, ms) {
|
|
@@ -848,6 +878,9 @@ function toDatetimeLocalValue(date) {
|
|
|
848
878
|
// (no dependency on hours, timezone or DST).
|
|
849
879
|
let days = calendarDaysExclusive(start, end);
|
|
850
880
|
|
|
881
|
+
// Maintenance is a simple ON/OFF flag per item (no dates).
|
|
882
|
+
const maintenance = new Set((items || []).filter(it => it && it.maintenance).map(it => String(it.id)));
|
|
883
|
+
|
|
851
884
|
// Fetch existing events overlapping the selection to disable already reserved items.
|
|
852
885
|
let blocked = new Set();
|
|
853
886
|
try {
|
|
@@ -878,16 +911,18 @@ function toDatetimeLocalValue(date) {
|
|
|
878
911
|
|
|
879
912
|
const rows = items.map((it, idx) => {
|
|
880
913
|
const id = String(it.id);
|
|
881
|
-
const
|
|
914
|
+
const isMaint = maintenance.has(id);
|
|
915
|
+
const disabled = blocked.has(id) || isMaint;
|
|
882
916
|
const priceTxt = fmtPrice(it.price || 0);
|
|
883
917
|
const safeName = String(it.name).replace(/</g, '<').replace(/>/g, '>');
|
|
918
|
+
const labelName = isMaint ? `🔧 ${safeName} (en maintenance)` : safeName;
|
|
884
919
|
return `
|
|
885
920
|
<div class="d-flex align-items-center py-1 ${disabled ? 'opacity-50' : ''}" data-itemid="${id}" style="border-bottom: 1px solid var(--bs-border-color, #ddd);">
|
|
886
921
|
<div style="width: 26px;">
|
|
887
922
|
<input type="checkbox" class="form-check-input onekite-item-cb" data-id="${id}" data-name="${safeName}" data-price="${String(it.price || 0)}" ${disabled ? 'disabled' : ''}>
|
|
888
923
|
</div>
|
|
889
924
|
<div class="flex-grow-1">
|
|
890
|
-
<div><strong>${
|
|
925
|
+
<div><strong>${labelName}</strong></div>
|
|
891
926
|
<div class="text-muted" style="font-size: 12px;">${priceTxt} € / jour</div>
|
|
892
927
|
</div>
|
|
893
928
|
</div>
|
|
@@ -1001,7 +1036,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1001
1036
|
|
|
1002
1037
|
document.querySelectorAll('#onekite-items [data-itemid]').forEach((row) => {
|
|
1003
1038
|
const iid = row.getAttribute('data-itemid');
|
|
1004
|
-
const dis = blocked.has(String(iid));
|
|
1039
|
+
const dis = blocked.has(String(iid)) || maintenance.has(String(iid));
|
|
1005
1040
|
row.classList.toggle('opacity-50', dis);
|
|
1006
1041
|
const cb = row.querySelector('input.onekite-item-cb');
|
|
1007
1042
|
if (cb) {
|
|
@@ -1065,6 +1100,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1065
1100
|
}
|
|
1066
1101
|
|
|
1067
1102
|
const items = await loadItems();
|
|
1103
|
+
cachedItems = items;
|
|
1068
1104
|
const caps = await loadCapabilities().catch(() => ({}));
|
|
1069
1105
|
const canCreateSpecial = !!caps.canCreateSpecial;
|
|
1070
1106
|
const canDeleteSpecial = !!caps.canDeleteSpecial;
|
|
@@ -1303,14 +1339,18 @@ function toDatetimeLocalValue(date) {
|
|
|
1303
1339
|
// If the user used "Durée rapide", the effective end date is held
|
|
1304
1340
|
// inside the dialog (returned as `chosen.endDate`).
|
|
1305
1341
|
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(info.end);
|
|
1306
|
-
await requestReservation({
|
|
1342
|
+
const resp = await requestReservation({
|
|
1307
1343
|
start: startDate,
|
|
1308
1344
|
end: endDate,
|
|
1309
1345
|
itemIds: chosen.itemIds,
|
|
1310
1346
|
itemNames: chosen.itemNames,
|
|
1311
1347
|
total: chosen.total,
|
|
1312
1348
|
});
|
|
1313
|
-
|
|
1349
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1350
|
+
showAlert('success', 'Réservation confirmée.');
|
|
1351
|
+
} else {
|
|
1352
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
1353
|
+
}
|
|
1314
1354
|
invalidateEventsCache();
|
|
1315
1355
|
scheduleRefetch(calendar);
|
|
1316
1356
|
calendar.unselect();
|
|
@@ -1865,6 +1905,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1865
1905
|
|
|
1866
1906
|
// Expose for live updates
|
|
1867
1907
|
try { window.oneKiteCalendar = calendar; } catch (e) {}
|
|
1908
|
+
currentCalendar = calendar;
|
|
1868
1909
|
|
|
1869
1910
|
calendar.render();
|
|
1870
1911
|
|
|
@@ -1959,11 +2000,201 @@ function toDatetimeLocalValue(date) {
|
|
|
1959
2000
|
}
|
|
1960
2001
|
|
|
1961
2002
|
// Auto-init on /calendar when ajaxify finishes rendering.
|
|
1962
|
-
|
|
2003
|
+
|
|
2004
|
+
function unmountMobileFab() {
|
|
2005
|
+
try {
|
|
2006
|
+
if (fabEl) {
|
|
2007
|
+
if (fabHandler) fabEl.removeEventListener('click', fabHandler);
|
|
2008
|
+
fabEl.remove();
|
|
2009
|
+
}
|
|
2010
|
+
} catch (e) {}
|
|
2011
|
+
fabEl = null;
|
|
2012
|
+
fabHandler = null;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function formatDdMmYyyy(d) {
|
|
2016
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
2017
|
+
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function parseDdMmYyyy(s) {
|
|
2021
|
+
const m = String(s || '').trim().match(/^([0-3]\d)\/([0-1]\d)\/(\d{4})$/);
|
|
2022
|
+
if (!m) return null;
|
|
2023
|
+
const day = parseInt(m[1], 10);
|
|
2024
|
+
const month = parseInt(m[2], 10);
|
|
2025
|
+
const year = parseInt(m[3], 10);
|
|
2026
|
+
if (month < 1 || month > 12) return null;
|
|
2027
|
+
const d = new Date(year, month - 1, day, 0, 0, 0, 0);
|
|
2028
|
+
// Validate (Date will overflow on invalid day)
|
|
2029
|
+
if (d.getFullYear() !== year || (d.getMonth() + 1) !== month || d.getDate() !== day) return null;
|
|
2030
|
+
return d;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
async function openFabDatePicker() {
|
|
2034
|
+
if (!lockAction('fab-date-picker', 700)) return;
|
|
2035
|
+
|
|
2036
|
+
// Defaults: tomorrow -> tomorrow (cannot book today or past)
|
|
2037
|
+
const today = new Date();
|
|
2038
|
+
today.setHours(0, 0, 0, 0);
|
|
2039
|
+
const minStart = new Date(today);
|
|
2040
|
+
minStart.setDate(minStart.getDate() + 1);
|
|
2041
|
+
|
|
2042
|
+
const html = `
|
|
2043
|
+
<div class="onekite-fab-dates">
|
|
2044
|
+
<div class="mb-2">
|
|
2045
|
+
<label class="form-label">Date de début</label>
|
|
2046
|
+
<input class="form-control" type="date" id="onekite-fab-start" autocomplete="off" />
|
|
2047
|
+
</div>
|
|
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>
|
|
2053
|
+
</div>
|
|
2054
|
+
`;
|
|
2055
|
+
|
|
2056
|
+
const dlg = bootbox.dialog({
|
|
2057
|
+
title: 'Choisir des dates',
|
|
2058
|
+
message: html,
|
|
2059
|
+
buttons: {
|
|
2060
|
+
cancel: { label: 'Annuler', className: 'btn-secondary' },
|
|
2061
|
+
ok: {
|
|
2062
|
+
label: 'Continuer',
|
|
2063
|
+
className: 'btn-primary',
|
|
2064
|
+
callback: function () {
|
|
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);
|
|
2069
|
+
if (!s || !e) {
|
|
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é.');
|
|
2077
|
+
return false;
|
|
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
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Convert end inclusive -> end exclusive (FullCalendar rule)
|
|
2085
|
+
const endExcl = new Date(e);
|
|
2086
|
+
endExcl.setDate(endExcl.getDate() + 1);
|
|
2087
|
+
|
|
2088
|
+
// Open the standard reservation dialog
|
|
2089
|
+
(async () => {
|
|
2090
|
+
try {
|
|
2091
|
+
if (isDialogOpen) return;
|
|
2092
|
+
isDialogOpen = true;
|
|
2093
|
+
const items = cachedItems || (await loadItems());
|
|
2094
|
+
const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
|
|
2095
|
+
if (chosen && chosen.itemIds && chosen.itemIds.length) {
|
|
2096
|
+
const startDate = toLocalYmd(s);
|
|
2097
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(endExcl);
|
|
2098
|
+
const resp = await requestReservation({
|
|
2099
|
+
start: startDate,
|
|
2100
|
+
end: endDate,
|
|
2101
|
+
itemIds: chosen.itemIds,
|
|
2102
|
+
itemNames: chosen.itemNames,
|
|
2103
|
+
total: chosen.total,
|
|
2104
|
+
});
|
|
2105
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
2106
|
+
showAlert('success', 'Réservation confirmée.');
|
|
2107
|
+
} else {
|
|
2108
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
2109
|
+
}
|
|
2110
|
+
invalidateEventsCache();
|
|
2111
|
+
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2112
|
+
}
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
// ignore
|
|
2115
|
+
} finally {
|
|
2116
|
+
isDialogOpen = false;
|
|
2117
|
+
}
|
|
2118
|
+
})();
|
|
2119
|
+
|
|
2120
|
+
// close picker modal
|
|
2121
|
+
return true;
|
|
2122
|
+
},
|
|
2123
|
+
},
|
|
2124
|
+
},
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
dlg.on('shown.bs.modal', () => {
|
|
2128
|
+
try {
|
|
2129
|
+
const startEl = document.getElementById('onekite-fab-start');
|
|
2130
|
+
const endEl = document.getElementById('onekite-fab-end');
|
|
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
|
+
}
|
|
2152
|
+
if (startEl) startEl.focus();
|
|
2153
|
+
} catch (e) {}
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
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
|
+
|
|
2171
|
+
function mountMobileFab() {
|
|
2172
|
+
try {
|
|
2173
|
+
unmountMobileFab();
|
|
2174
|
+
// Only on mobile screens
|
|
2175
|
+
if (!window.matchMedia || !window.matchMedia('(max-width: 768px)').matches) return;
|
|
2176
|
+
|
|
2177
|
+
fabEl = document.createElement('button');
|
|
2178
|
+
fabEl.type = 'button';
|
|
2179
|
+
fabEl.className = 'btn btn-primary onekite-fab';
|
|
2180
|
+
fabEl.setAttribute('aria-label', 'Nouvelle réservation');
|
|
2181
|
+
fabEl.innerHTML = '<i class="fa fa-plus"></i>';
|
|
2182
|
+
|
|
2183
|
+
fabHandler = () => openFabDatePicker();
|
|
2184
|
+
fabEl.addEventListener('click', fabHandler);
|
|
2185
|
+
|
|
2186
|
+
document.body.appendChild(fabEl);
|
|
2187
|
+
} catch (e) {}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
function autoInit(data) {
|
|
1963
2191
|
try {
|
|
1964
2192
|
const tpl = data && data.template ? data.template.name : (ajaxify && ajaxify.data && ajaxify.data.template ? ajaxify.data.template.name : '');
|
|
1965
2193
|
if (tpl === 'calendar-onekite') {
|
|
1966
2194
|
init('#onekite-calendar');
|
|
2195
|
+
mountMobileFab();
|
|
2196
|
+
} else {
|
|
2197
|
+
unmountMobileFab();
|
|
1967
2198
|
}
|
|
1968
2199
|
} catch (e) {}
|
|
1969
2200
|
}
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
<li class="nav-item" role="presentation">
|
|
21
21
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-accounting" type="button" role="tab">Comptabilisation</button>
|
|
22
22
|
</li>
|
|
23
|
+
<li class="nav-item" role="presentation">
|
|
24
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-maintenance" type="button" role="tab">Maintenance</button>
|
|
25
|
+
</li>
|
|
26
|
+
<li class="nav-item" role="presentation">
|
|
27
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#onekite-tab-audit" type="button" role="tab">Audit</button>
|
|
28
|
+
</li>
|
|
23
29
|
</ul>
|
|
24
30
|
|
|
25
31
|
<form id="onekite-settings-form">
|
|
@@ -53,6 +59,12 @@
|
|
|
53
59
|
<div class="form-text">Après validation (statut <code>paiement en attente</code>), un rappel est envoyé après ce délai. La réservation est ensuite expirée après <strong>2×</strong> ce délai.</div>
|
|
54
60
|
</div>
|
|
55
61
|
|
|
62
|
+
<div class="mb-3">
|
|
63
|
+
<label class="form-label">Location longue durée (jours) pour validateurs</label>
|
|
64
|
+
<input class="form-control" name="validatorFreeMaxDays" placeholder="0">
|
|
65
|
+
<div class="form-text">Les validateurs ne paient pas leurs propres locations <strong>jusqu’à</strong> ce nombre de jours (inclus). Au-delà, le workflow normal (demande → validation → paiement) s’applique. Mets <code>0</code> pour toujours gratuit.</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
56
68
|
<hr class="my-4" />
|
|
57
69
|
<h4>Discord</h4>
|
|
58
70
|
|
|
@@ -207,6 +219,68 @@
|
|
|
207
219
|
<tbody></tbody>
|
|
208
220
|
</table>
|
|
209
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>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div class="tab-pane fade" id="onekite-tab-maintenance" role="tabpanel">
|
|
236
|
+
<h4>Maintenance (blocage manuel)</h4>
|
|
237
|
+
<div class="form-text mb-3">Active la maintenance sur un matériel pour le rendre indisponible (sans notion de dates). Les utilisateurs verront <strong>🔧 (en maintenance)</strong> dans la modale de réservation.</div>
|
|
238
|
+
|
|
239
|
+
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
|
240
|
+
<input class="form-control" id="onekite-maint-search" placeholder="Rechercher un matériel..." style="max-width: 420px;">
|
|
241
|
+
<button type="button" class="btn btn-outline-secondary" id="onekite-maint-refresh">Rafraîchir</button>
|
|
242
|
+
<div class="vr d-none d-md-block"></div>
|
|
243
|
+
<button type="button" class="btn btn-outline-warning" id="onekite-maint-all-on" title="Active la maintenance sur tous les matériels">Tout mettre en maintenance</button>
|
|
244
|
+
<button type="button" class="btn btn-outline-success" id="onekite-maint-all-off" title="Désactive la maintenance sur tous les matériels">Tout enlever de maintenance</button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="table-responsive">
|
|
248
|
+
<table class="table table-sm" id="onekite-maint-table">
|
|
249
|
+
<thead>
|
|
250
|
+
<tr><th>Matériel</th><th style="width: 140px;">Maintenance</th></tr>
|
|
251
|
+
</thead>
|
|
252
|
+
<tbody></tbody>
|
|
253
|
+
</table>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div class="tab-pane fade" id="onekite-tab-audit" role="tabpanel">
|
|
258
|
+
<h4>Journal d’audit</h4>
|
|
259
|
+
<div class="form-text mb-3">Historique des actions (demandes, validations, refus, annulations, maintenance). Purge possible par année.</div>
|
|
260
|
+
|
|
261
|
+
<div class="d-flex flex-wrap gap-2 align-items-end mb-3">
|
|
262
|
+
<div>
|
|
263
|
+
<label class="form-label">Année</label>
|
|
264
|
+
<input class="form-control" id="onekite-audit-year" placeholder="YYYY" style="max-width: 160px;">
|
|
265
|
+
</div>
|
|
266
|
+
<div>
|
|
267
|
+
<label class="form-label">Recherche</label>
|
|
268
|
+
<input class="form-control" id="onekite-audit-search" placeholder="pseudo, matériel, action..." style="max-width: 360px;">
|
|
269
|
+
</div>
|
|
270
|
+
<div>
|
|
271
|
+
<button type="button" class="btn btn-primary" id="onekite-audit-refresh">Rafraîchir</button>
|
|
272
|
+
<button type="button" class="btn btn-outline-danger" id="onekite-audit-purge">Purger l’année</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="table-responsive">
|
|
277
|
+
<table class="table table-sm" id="onekite-audit-table">
|
|
278
|
+
<thead>
|
|
279
|
+
<tr><th>Date</th><th>Acteur</th><th>Action</th><th>Cible</th><th>Détails</th></tr>
|
|
280
|
+
</thead>
|
|
281
|
+
<tbody></tbody>
|
|
282
|
+
</table>
|
|
283
|
+
</div>
|
|
210
284
|
</div>
|
|
211
285
|
</div>
|
|
212
286
|
</form>
|