nodebb-plugin-onekite-calendar 2.0.41 → 2.0.43
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/lib/admin.js +18 -0
- package/lib/api.js +227 -57
- package/lib/db.js +36 -0
- package/lib/discord.js +55 -27
- package/library.js +7 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/admin.js +36 -0
- package/public/client.js +282 -350
- package/templates/admin/plugins/calendar-onekite.tpl +32 -5
package/public/client.js
CHANGED
|
@@ -208,12 +208,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
208
208
|
return opts.join('');
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
async function openSpecialEventDialog(selectionInfo
|
|
212
|
-
const options = opts || {};
|
|
213
|
-
const kind = (String(options.kind || 'event').trim().toLowerCase() === 'outing') ? 'outing' : 'event';
|
|
214
|
-
const dialogTitle = String(options.dialogTitle || (kind === 'outing' ? 'Créer une prévision de sortie' : 'Créer un évènement'));
|
|
215
|
-
const defaultTitle = String(options.defaultTitle || (kind === 'outing' ? 'Sortie' : 'Évènement'));
|
|
216
|
-
|
|
211
|
+
async function openSpecialEventDialog(selectionInfo) {
|
|
217
212
|
const start = selectionInfo.start;
|
|
218
213
|
// FullCalendar can omit `end` for certain interactions. Also, for all-day
|
|
219
214
|
// selections, `end` is exclusive (next day at 00:00). We normalise below
|
|
@@ -267,7 +262,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
267
262
|
const html = `
|
|
268
263
|
<div class="mb-3">
|
|
269
264
|
<label class="form-label">Titre</label>
|
|
270
|
-
<input type="text" class="form-control" id="onekite-se-title" placeholder="Ex: ..."
|
|
265
|
+
<input type="text" class="form-control" id="onekite-se-title" placeholder="Ex: ..." />
|
|
271
266
|
</div>
|
|
272
267
|
<div class="row g-2">
|
|
273
268
|
<div class="col-12 col-md-6">
|
|
@@ -312,7 +307,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
312
307
|
return await new Promise((resolve) => {
|
|
313
308
|
let resolved = false;
|
|
314
309
|
const dialog = bootbox.dialog({
|
|
315
|
-
title:
|
|
310
|
+
title: 'Créer un évènement',
|
|
316
311
|
message: html,
|
|
317
312
|
buttons: {
|
|
318
313
|
cancel: {
|
|
@@ -361,7 +356,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
361
356
|
const lat = (document.getElementById('onekite-se-lat')?.value || '').trim();
|
|
362
357
|
const lon = (document.getElementById('onekite-se-lon')?.value || '').trim();
|
|
363
358
|
resolved = true;
|
|
364
|
-
resolve({
|
|
359
|
+
resolve({ title, start: startVal, end: endVal, address, notes, lat, lon });
|
|
365
360
|
return true;
|
|
366
361
|
},
|
|
367
362
|
},
|
|
@@ -432,6 +427,17 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
432
427
|
});
|
|
433
428
|
}
|
|
434
429
|
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
async function openOutingDialog(selectionInfo) {
|
|
433
|
+
// Same UI as special events, but saved as "Sorties" (prévisions).
|
|
434
|
+
const payload = await openSpecialEventDialog(selectionInfo);
|
|
435
|
+
if (!payload) return null;
|
|
436
|
+
// Default title if empty
|
|
437
|
+
if (!payload.title) payload.title = 'Prévision de sortie';
|
|
438
|
+
return payload;
|
|
439
|
+
}
|
|
440
|
+
|
|
435
441
|
async function openMapViewer(title, address, lat, lon) {
|
|
436
442
|
const mapId = `onekite-map-view-${Date.now()}-${Math.floor(Math.random()*10000)}`;
|
|
437
443
|
const safeAddr = (address || '').trim();
|
|
@@ -1163,97 +1169,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1163
1169
|
const canCreateSpecial = !!caps.canCreateSpecial;
|
|
1164
1170
|
const canDeleteSpecial = !!caps.canDeleteSpecial;
|
|
1165
1171
|
|
|
1166
|
-
//
|
|
1167
|
-
// Persist the user's last choice so actions (create/approve/refuse/refetch) don't reset it.
|
|
1168
|
-
const modeStorageKey = (() => {
|
|
1169
|
-
try {
|
|
1170
|
-
const uid = (typeof app !== 'undefined' && app && app.user && (app.user.uid || app.user.uid === 0)) ? String(app.user.uid)
|
|
1171
|
-
: (window.ajaxify && window.ajaxify.data && (window.ajaxify.data.uid || window.ajaxify.data.uid === 0)) ? String(window.ajaxify.data.uid)
|
|
1172
|
-
: '0';
|
|
1173
|
-
return `onekiteCalendarMode:${uid}`;
|
|
1174
|
-
} catch (e) {
|
|
1175
|
-
return 'onekiteCalendarMode:0';
|
|
1176
|
-
}
|
|
1177
|
-
})();
|
|
1178
|
-
|
|
1179
|
-
function loadSavedMode() {
|
|
1180
|
-
try {
|
|
1181
|
-
const v = (window.localStorage && window.localStorage.getItem(modeStorageKey)) || '';
|
|
1182
|
-
return (v === 'special' || v === 'reservation') ? v : 'reservation';
|
|
1183
|
-
} catch (e) {
|
|
1184
|
-
return 'reservation';
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function saveMode(v) {
|
|
1189
|
-
try {
|
|
1190
|
-
if (!window.localStorage) return;
|
|
1191
|
-
window.localStorage.setItem(modeStorageKey, v);
|
|
1192
|
-
} catch (e) {
|
|
1193
|
-
// ignore
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
let mode = loadSavedMode();
|
|
1198
|
-
// Avoid showing the "mode évènement" hint multiple times (desktop + mobile handlers)
|
|
1199
|
-
let lastModeHintAt = 0;
|
|
1200
|
-
|
|
1201
|
-
function refreshDesktopModeButton() {
|
|
1202
|
-
try {
|
|
1203
|
-
const btn = document.querySelector('#onekite-calendar .fc-newSpecial-button');
|
|
1204
|
-
if (!btn) return;
|
|
1205
|
-
|
|
1206
|
-
const isSpecial = mode === 'special';
|
|
1207
|
-
const label = isSpecial ? 'Évènement ✓' : 'Évènement';
|
|
1208
|
-
|
|
1209
|
-
// Ensure a single canonical .fc-button-text (prevents "ÉvènementÉvènement" after rerenders)
|
|
1210
|
-
let span = btn.querySelector('.fc-button-text');
|
|
1211
|
-
if (!span) {
|
|
1212
|
-
span = document.createElement('span');
|
|
1213
|
-
span.className = 'fc-button-text';
|
|
1214
|
-
// Remove stray text nodes before inserting span
|
|
1215
|
-
[...btn.childNodes].forEach((n) => {
|
|
1216
|
-
if (n && n.nodeType === Node.TEXT_NODE) n.remove();
|
|
1217
|
-
});
|
|
1218
|
-
btn.appendChild(span);
|
|
1219
|
-
} else {
|
|
1220
|
-
// Remove any stray text nodes beside the span
|
|
1221
|
-
[...btn.childNodes].forEach((n) => {
|
|
1222
|
-
if (n && n.nodeType === Node.TEXT_NODE && n.textContent.trim()) n.remove();
|
|
1223
|
-
});
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
span.textContent = label;
|
|
1227
|
-
btn.classList.toggle('onekite-active', isSpecial);
|
|
1228
|
-
} catch (e) {}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
function setMode(next, opts) {
|
|
1232
|
-
if (next !== 'reservation' && next !== 'special') return;
|
|
1233
|
-
mode = next;
|
|
1234
|
-
saveMode(mode);
|
|
1235
|
-
|
|
1236
|
-
// Reset any pending selection/dialog state when switching modes
|
|
1237
|
-
try { if (mode === 'reservation') { calendar.unselect(); isDialogOpen = false; } } catch (e) {}
|
|
1238
|
-
|
|
1239
|
-
const silent = !!(opts && opts.silent);
|
|
1240
|
-
if (!silent && mode === 'special') {
|
|
1241
|
-
const now = Date.now();
|
|
1242
|
-
if (now - lastModeHintAt > 1200) {
|
|
1243
|
-
lastModeHintAt = now;
|
|
1244
|
-
showAlert('success', 'Mode évènement : sélectionnez une date ou une plage');
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
refreshDesktopModeButton();
|
|
1248
|
-
try {
|
|
1249
|
-
const mb = document.querySelector('#onekite-mobile-controls .onekite-mode-btn');
|
|
1250
|
-
if (mb) {
|
|
1251
|
-
const isSpecial = mode === 'special';
|
|
1252
|
-
mb.textContent = isSpecial ? 'Mode évènement ✓' : 'Mode évènement';
|
|
1253
|
-
mb.classList.toggle('onekite-active', isSpecial);
|
|
1254
|
-
}
|
|
1255
|
-
} catch (e) {}
|
|
1256
|
-
}
|
|
1172
|
+
// Creation chooser: Location / Prévision de sortie / Évènement (si autorisé).
|
|
1257
1173
|
|
|
1258
1174
|
// Inject lightweight responsive CSS once.
|
|
1259
1175
|
try {
|
|
@@ -1320,149 +1236,29 @@ function toDatetimeLocalValue(date) {
|
|
|
1320
1236
|
left: 'prev,next today',
|
|
1321
1237
|
center: 'title',
|
|
1322
1238
|
// Only month + week (no day view)
|
|
1323
|
-
right:
|
|
1239
|
+
right: 'dayGridMonth,timeGridWeek',
|
|
1324
1240
|
};
|
|
1325
1241
|
|
|
1326
1242
|
let calendar;
|
|
1327
1243
|
|
|
1328
|
-
// Unified handler for creation actions
|
|
1329
|
-
// On mobile, FullCalendar may emit `dateClick` but not `select` for a simple tap.
|
|
1330
|
-
// We therefore support both without calling `calendar.select()` (which could
|
|
1331
|
-
// double-trigger `select`).
|
|
1244
|
+
// Unified handler for creation actions via a chooser modal.
|
|
1332
1245
|
async function handleCreateFromSelection(info) {
|
|
1333
|
-
if (isDialogOpen)
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
//
|
|
1337
|
-
if (!lockAction('create', 900)) {
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1340
|
-
isDialogOpen = true;
|
|
1246
|
+
if (isDialogOpen) return;
|
|
1247
|
+
if (!lockAction('create', 900)) return;
|
|
1248
|
+
|
|
1249
|
+
// Business rule: nothing can be created in the past.
|
|
1341
1250
|
try {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}
|
|
1349
|
-
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1350
|
-
method: 'POST',
|
|
1351
|
-
body: JSON.stringify(payload),
|
|
1352
|
-
}).catch(async () => {
|
|
1353
|
-
return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1354
|
-
method: 'POST',
|
|
1355
|
-
body: JSON.stringify(payload),
|
|
1356
|
-
});
|
|
1357
|
-
});
|
|
1358
|
-
showAlert('success', 'Évènement créé.');
|
|
1359
|
-
invalidateEventsCache();
|
|
1360
|
-
scheduleRefetch(calendar);
|
|
1361
|
-
calendar.unselect();
|
|
1362
|
-
isDialogOpen = false;
|
|
1251
|
+
const startDateCheck = toLocalYmd(info.start);
|
|
1252
|
+
const todayCheck = toLocalYmd(new Date());
|
|
1253
|
+
if (startDateCheck < todayCheck) {
|
|
1254
|
+
lastDateRuleToastAt = Date.now();
|
|
1255
|
+
showAlert('error', "Impossible de créer pour une date passée.");
|
|
1256
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1363
1257
|
return;
|
|
1364
1258
|
}
|
|
1259
|
+
} catch (e) {}
|
|
1365
1260
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
// On a day click / selection in reservation mode, propose the user to create either
|
|
1369
|
-
// a Location (reservation) or a "Prévision de sortie" (special event).
|
|
1370
|
-
try {
|
|
1371
|
-
const choice = await new Promise((resolve) => {
|
|
1372
|
-
bootbox.dialog({
|
|
1373
|
-
title: 'Créer',
|
|
1374
|
-
message: '<div>Que veux-tu créer ?</div>',
|
|
1375
|
-
buttons: {
|
|
1376
|
-
cancel: { label: 'Annuler', className: 'btn-secondary', callback: () => resolve(null) },
|
|
1377
|
-
outing: { label: 'Prévision de sortie', className: 'btn-info', callback: () => resolve('outing') },
|
|
1378
|
-
reservation: { label: 'Location', className: 'btn-primary', callback: () => resolve('reservation') },
|
|
1379
|
-
},
|
|
1380
|
-
});
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
if (!choice) {
|
|
1384
|
-
calendar.unselect();
|
|
1385
|
-
isDialogOpen = false;
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
if (choice === 'outing') {
|
|
1390
|
-
const payload = await openSpecialEventDialog(info, { kind: 'outing', dialogTitle: 'Créer une prévision de sortie', defaultTitle: 'Sortie' });
|
|
1391
|
-
if (!payload) {
|
|
1392
|
-
calendar.unselect();
|
|
1393
|
-
isDialogOpen = false;
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1397
|
-
method: 'POST',
|
|
1398
|
-
body: JSON.stringify(payload),
|
|
1399
|
-
}).catch(async () => {
|
|
1400
|
-
return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1401
|
-
method: 'POST',
|
|
1402
|
-
body: JSON.stringify(payload),
|
|
1403
|
-
});
|
|
1404
|
-
});
|
|
1405
|
-
showAlert('success', 'Sortie créée.');
|
|
1406
|
-
invalidateEventsCache();
|
|
1407
|
-
scheduleRefetch(calendar);
|
|
1408
|
-
calendar.unselect();
|
|
1409
|
-
isDialogOpen = false;
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
} catch (e) {
|
|
1413
|
-
// ignore
|
|
1414
|
-
}
|
|
1415
|
-
// Business rule: reservations cannot start in the past.
|
|
1416
|
-
// (We validate again on the server, but this gives immediate feedback.)
|
|
1417
|
-
try {
|
|
1418
|
-
const startDateCheck = toLocalYmd(info.start);
|
|
1419
|
-
const todayCheck = toLocalYmd(new Date());
|
|
1420
|
-
if (startDateCheck < todayCheck) {
|
|
1421
|
-
lastDateRuleToastAt = Date.now();
|
|
1422
|
-
showAlert('error', "Impossible de réserver pour une date passée.");
|
|
1423
|
-
calendar.unselect();
|
|
1424
|
-
isDialogOpen = false;
|
|
1425
|
-
return;
|
|
1426
|
-
}
|
|
1427
|
-
} catch (e) {
|
|
1428
|
-
// ignore
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
if (!items || !items.length) {
|
|
1432
|
-
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1433
|
-
calendar.unselect();
|
|
1434
|
-
isDialogOpen = false;
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
const chosen = await openReservationDialog(info, items);
|
|
1438
|
-
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) {
|
|
1439
|
-
calendar.unselect();
|
|
1440
|
-
isDialogOpen = false;
|
|
1441
|
-
return;
|
|
1442
|
-
}
|
|
1443
|
-
// Send date strings (no hours) so reservations are day-based.
|
|
1444
|
-
const startDate = toLocalYmd(info.start);
|
|
1445
|
-
// NOTE: FullCalendar's `info.end` reflects the original selection.
|
|
1446
|
-
// If the user used "Durée rapide", the effective end date is held
|
|
1447
|
-
// inside the dialog (returned as `chosen.endDate`).
|
|
1448
|
-
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(info.end);
|
|
1449
|
-
const resp = await requestReservation({
|
|
1450
|
-
start: startDate,
|
|
1451
|
-
end: endDate,
|
|
1452
|
-
itemIds: chosen.itemIds,
|
|
1453
|
-
itemNames: chosen.itemNames,
|
|
1454
|
-
total: chosen.total,
|
|
1455
|
-
});
|
|
1456
|
-
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1457
|
-
showAlert('success', 'Réservation confirmée.');
|
|
1458
|
-
} else {
|
|
1459
|
-
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
1460
|
-
}
|
|
1461
|
-
invalidateEventsCache();
|
|
1462
|
-
scheduleRefetch(calendar);
|
|
1463
|
-
calendar.unselect();
|
|
1464
|
-
isDialogOpen = false;
|
|
1465
|
-
} catch (e) {
|
|
1261
|
+
function handleCreateError(e) {
|
|
1466
1262
|
const code = String((e && (e.status || e.message)) || '');
|
|
1467
1263
|
const payload = e && e.payload ? e.payload : null;
|
|
1468
1264
|
|
|
@@ -1470,33 +1266,129 @@ function toDatetimeLocalValue(date) {
|
|
|
1470
1266
|
const msg = payload && (payload.message || payload.error || payload.msg) ? String(payload.message || payload.error || payload.msg) : '';
|
|
1471
1267
|
const c = payload && payload.code ? String(payload.code) : '';
|
|
1472
1268
|
if (c === 'NOT_MEMBER' || /adh(é|e)rent/i.test(msg) || /membership/i.test(msg)) {
|
|
1473
|
-
showAlert('error', msg || 'Vous devez être adhérent pour pouvoir effectuer
|
|
1269
|
+
showAlert('error', msg || 'Vous devez être adhérent pour pouvoir effectuer cette action.');
|
|
1474
1270
|
} else {
|
|
1475
|
-
showAlert('error', msg || '
|
|
1271
|
+
showAlert('error', msg || 'Action impossible : droits insuffisants (groupe).');
|
|
1476
1272
|
}
|
|
1477
1273
|
} else if (code === '409') {
|
|
1478
|
-
showAlert('error', 'Impossible :
|
|
1274
|
+
showAlert('error', 'Impossible : conflit de réservation sur cette période.');
|
|
1479
1275
|
} else if (code === '400' && payload && (payload.error === 'date-too-soon' || payload.code === 'date-too-soon')) {
|
|
1480
|
-
// If we already showed the client-side toast a moment ago, avoid a duplicate.
|
|
1481
1276
|
if (!lastDateRuleToastAt || (Date.now() - lastDateRuleToastAt) > 1500) {
|
|
1482
|
-
showAlert('error', String(payload.message || "Impossible de
|
|
1277
|
+
showAlert('error', String(payload.message || "Impossible de créer pour une date passée."));
|
|
1483
1278
|
}
|
|
1484
1279
|
} else {
|
|
1485
1280
|
const msgRaw = payload && (payload.message || payload.error || payload.msg)
|
|
1486
1281
|
? String(payload.message || payload.error || payload.msg)
|
|
1487
1282
|
: '';
|
|
1488
|
-
|
|
1489
|
-
// NodeBB can return a plain "not-logged-in" string when the user is not authenticated.
|
|
1490
|
-
// We want a user-friendly message consistent with the membership requirement.
|
|
1491
1283
|
const msg = (/\bnot-logged-in\b/i.test(msgRaw) || /\[\[error:not-logged-in\]\]/i.test(msgRaw))
|
|
1492
1284
|
? 'Vous devez être adhérent Onekite'
|
|
1493
1285
|
: msgRaw;
|
|
1494
|
-
|
|
1495
|
-
showAlert('error', msg || ((e && (e.status === 401 || e.status === 403)) ? 'Vous devez être adhérent Onekite' : 'Erreur lors de la création de la demande.'));
|
|
1286
|
+
showAlert('error', msg || 'Erreur lors de la création.');
|
|
1496
1287
|
}
|
|
1497
|
-
calendar.unselect();
|
|
1498
|
-
isDialogOpen = false;
|
|
1499
1288
|
}
|
|
1289
|
+
|
|
1290
|
+
const buttons = {
|
|
1291
|
+
close: { label: 'Annuler', className: 'btn-secondary' },
|
|
1292
|
+
location: {
|
|
1293
|
+
label: 'Location',
|
|
1294
|
+
className: 'btn-primary',
|
|
1295
|
+
callback: async () => {
|
|
1296
|
+
try {
|
|
1297
|
+
isDialogOpen = true;
|
|
1298
|
+
if (!items || !items.length) {
|
|
1299
|
+
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const chosen = await openReservationDialog(info, items);
|
|
1303
|
+
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
|
|
1304
|
+
const startDate = toLocalYmd(info.start);
|
|
1305
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(info.end);
|
|
1306
|
+
const resp = await requestReservation({
|
|
1307
|
+
start: startDate,
|
|
1308
|
+
end: endDate,
|
|
1309
|
+
itemIds: chosen.itemIds,
|
|
1310
|
+
itemNames: chosen.itemNames,
|
|
1311
|
+
total: chosen.total,
|
|
1312
|
+
});
|
|
1313
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1314
|
+
showAlert('success', 'Réservation confirmée.');
|
|
1315
|
+
} else {
|
|
1316
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
1317
|
+
}
|
|
1318
|
+
invalidateEventsCache();
|
|
1319
|
+
scheduleRefetch(calendar);
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
handleCreateError(e);
|
|
1322
|
+
} finally {
|
|
1323
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1324
|
+
isDialogOpen = false;
|
|
1325
|
+
}
|
|
1326
|
+
return true;
|
|
1327
|
+
},
|
|
1328
|
+
},
|
|
1329
|
+
outing: {
|
|
1330
|
+
label: 'Prévision de sortie',
|
|
1331
|
+
className: 'btn-outline-primary',
|
|
1332
|
+
callback: async () => {
|
|
1333
|
+
try {
|
|
1334
|
+
isDialogOpen = true;
|
|
1335
|
+
const payload = await openOutingDialog(info);
|
|
1336
|
+
if (!payload) return;
|
|
1337
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/outings', {
|
|
1338
|
+
method: 'POST',
|
|
1339
|
+
body: JSON.stringify(payload),
|
|
1340
|
+
});
|
|
1341
|
+
showAlert('success', 'Prévision de sortie créée.');
|
|
1342
|
+
invalidateEventsCache();
|
|
1343
|
+
scheduleRefetch(calendar);
|
|
1344
|
+
} catch (e) {
|
|
1345
|
+
handleCreateError(e);
|
|
1346
|
+
} finally {
|
|
1347
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1348
|
+
isDialogOpen = false;
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
if (canCreateSpecial) {
|
|
1356
|
+
buttons.special = {
|
|
1357
|
+
label: 'Évènement',
|
|
1358
|
+
className: 'btn-outline-secondary',
|
|
1359
|
+
callback: async () => {
|
|
1360
|
+
try {
|
|
1361
|
+
isDialogOpen = true;
|
|
1362
|
+
const payload = await openSpecialEventDialog(info);
|
|
1363
|
+
if (!payload) return;
|
|
1364
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1365
|
+
method: 'POST',
|
|
1366
|
+
body: JSON.stringify(payload),
|
|
1367
|
+
}).catch(async () => {
|
|
1368
|
+
return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1369
|
+
method: 'POST',
|
|
1370
|
+
body: JSON.stringify(payload),
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
showAlert('success', 'Évènement créé.');
|
|
1374
|
+
invalidateEventsCache();
|
|
1375
|
+
scheduleRefetch(calendar);
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
handleCreateError(e);
|
|
1378
|
+
} finally {
|
|
1379
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1380
|
+
isDialogOpen = false;
|
|
1381
|
+
}
|
|
1382
|
+
return true;
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
bootbox.dialog({
|
|
1388
|
+
title: 'Créer',
|
|
1389
|
+
message: '<div class="text-muted">Que veux-tu créer sur ces dates ?</div>',
|
|
1390
|
+
buttons,
|
|
1391
|
+
});
|
|
1500
1392
|
}
|
|
1501
1393
|
|
|
1502
1394
|
calendar = new FullCalendar.Calendar(el, {
|
|
@@ -1509,14 +1401,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1509
1401
|
headerToolbar: headerToolbar,
|
|
1510
1402
|
// Keep titles short on mobile to avoid horizontal overflow
|
|
1511
1403
|
titleFormat: isMobileNow() ? { year: 'numeric', month: 'short' } : undefined,
|
|
1512
|
-
customButtons:
|
|
1513
|
-
newSpecial: {
|
|
1514
|
-
text: 'Évènement',
|
|
1515
|
-
click: () => {
|
|
1516
|
-
setMode((mode === 'special') ? 'reservation' : 'special');
|
|
1517
|
-
},
|
|
1518
|
-
},
|
|
1519
|
-
} : {},
|
|
1404
|
+
customButtons: {},
|
|
1520
1405
|
// We display the time ourselves inside the title for "special" events,
|
|
1521
1406
|
// to match reservation icons and avoid FullCalendar's fixed-width time column.
|
|
1522
1407
|
displayEventTime: false,
|
|
@@ -1598,19 +1483,15 @@ function toDatetimeLocalValue(date) {
|
|
|
1598
1483
|
}
|
|
1599
1484
|
},
|
|
1600
1485
|
eventDidMount: function (arg) {
|
|
1601
|
-
// Keep special event colors consistent
|
|
1486
|
+
// Keep special event colors consistent.
|
|
1602
1487
|
try {
|
|
1603
1488
|
const ev = arg && arg.event;
|
|
1604
1489
|
if (!ev) return;
|
|
1605
1490
|
if (ev.extendedProps && ev.extendedProps.type === 'special') {
|
|
1606
|
-
const kind = String((ev.extendedProps.kind || 'event'));
|
|
1607
|
-
const palette = (kind === 'outing')
|
|
1608
|
-
? { bg: '#0d6efd', border: '#0d6efd' }
|
|
1609
|
-
: { bg: '#8e44ad', border: '#8e44ad' };
|
|
1610
1491
|
const el2 = arg.el;
|
|
1611
1492
|
if (el2 && el2.style) {
|
|
1612
|
-
el2.style.backgroundColor =
|
|
1613
|
-
el2.style.borderColor =
|
|
1493
|
+
el2.style.backgroundColor = '#8e44ad';
|
|
1494
|
+
el2.style.borderColor = '#8e44ad';
|
|
1614
1495
|
el2.style.color = '#ffffff';
|
|
1615
1496
|
}
|
|
1616
1497
|
}
|
|
@@ -1643,7 +1524,13 @@ function toDatetimeLocalValue(date) {
|
|
|
1643
1524
|
} else if (p0.type === 'special' && p0.eid) {
|
|
1644
1525
|
const details = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(String(p0.eid))}`);
|
|
1645
1526
|
p = Object.assign({}, p0, details, {
|
|
1646
|
-
|
|
1527
|
+
pickupAddress: details.address || details.pickupAddress || p0.pickupAddress,
|
|
1528
|
+
pickupLat: details.lat || details.pickupLat || p0.pickupLat,
|
|
1529
|
+
pickupLon: details.lon || details.pickupLon || p0.pickupLon,
|
|
1530
|
+
});
|
|
1531
|
+
} else if (p0.type === 'outing' && p0.oid) {
|
|
1532
|
+
const details = await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(String(p0.oid))}`);
|
|
1533
|
+
p = Object.assign({}, p0, details, {
|
|
1647
1534
|
pickupAddress: details.address || details.pickupAddress || p0.pickupAddress,
|
|
1648
1535
|
pickupLat: details.lat || details.pickupLat || p0.pickupLat,
|
|
1649
1536
|
pickupLon: details.lon || details.pickupLon || p0.pickupLon,
|
|
@@ -1656,55 +1543,106 @@ function toDatetimeLocalValue(date) {
|
|
|
1656
1543
|
|
|
1657
1544
|
try {
|
|
1658
1545
|
if (p.type === 'special') {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1546
|
+
const username = String(p.username || '').trim();
|
|
1547
|
+
const userLine = username
|
|
1548
|
+
? `<div class="mb-2"><strong>Créé par</strong><br><a class="onekite-user-link" href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
|
|
1549
|
+
: '';
|
|
1550
|
+
const addr = String(p.pickupAddress || p.address || '').trim();
|
|
1551
|
+
const lat = Number(p.pickupLat || p.lat);
|
|
1552
|
+
const lon = Number(p.pickupLon || p.lon);
|
|
1553
|
+
const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
|
|
1554
|
+
const notes = String(p.notes || '').trim();
|
|
1555
|
+
const addrHtml = addr
|
|
1556
|
+
? (hasCoords
|
|
1557
|
+
? `<a href="#" class="onekite-map-link" data-address="${escapeHtml(addr)}" data-lat="${escapeHtml(String(lat))}" data-lon="${escapeHtml(String(lon))}">${escapeHtml(addr)}</a>`
|
|
1558
|
+
: `${escapeHtml(addr)}`)
|
|
1559
|
+
: '';
|
|
1560
|
+
const html = `
|
|
1561
|
+
<div class="mb-2"><strong>Titre</strong><br>${escapeHtml(p.title || ev.title || '')}</div>
|
|
1562
|
+
${userLine}
|
|
1563
|
+
<div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatDtWithTime(ev.start))} → ${escapeHtml(formatDtWithTime(ev.end))}</div>
|
|
1564
|
+
${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${addrHtml}</div>` : ''}
|
|
1565
|
+
${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
|
|
1566
|
+
`;
|
|
1567
|
+
const canDel = !!(p.canDeleteSpecial || canDeleteSpecial);
|
|
1568
|
+
bootbox.dialog({
|
|
1569
|
+
title: 'Évènement',
|
|
1570
|
+
message: html,
|
|
1571
|
+
buttons: {
|
|
1572
|
+
close: { label: 'Fermer', className: 'btn-secondary' },
|
|
1573
|
+
...(canDel ? {
|
|
1574
|
+
del: {
|
|
1575
|
+
label: 'Supprimer',
|
|
1576
|
+
className: 'btn-danger',
|
|
1577
|
+
callback: async () => {
|
|
1578
|
+
try {
|
|
1579
|
+
const eid = String(p.eid || ev.id).replace(/^special:/, '');
|
|
1580
|
+
await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}`, { method: 'DELETE' });
|
|
1581
|
+
showAlert('success', 'Évènement supprimé.');
|
|
1582
|
+
calendar.refetchEvents();
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
showAlert('error', 'Suppression impossible.');
|
|
1585
|
+
}
|
|
1586
|
+
},
|
|
1701
1587
|
},
|
|
1702
|
-
},
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1588
|
+
} : {}),
|
|
1589
|
+
},
|
|
1590
|
+
});
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
if (p.type === 'outing') {
|
|
1595
|
+
const username = String(p.username || '').trim();
|
|
1596
|
+
const userLine = username
|
|
1597
|
+
? `<div class="mb-2"><strong>Créé par</strong><br><a class="onekite-user-link" href="${window.location.origin}/user/${encodeURIComponent(username)}">${escapeHtml(username)}</a></div>`
|
|
1598
|
+
: '';
|
|
1599
|
+
const addr = String(p.address || p.pickupAddress || '').trim();
|
|
1600
|
+
const lat = Number(p.lat || p.pickupLat);
|
|
1601
|
+
const lon = Number(p.lon || p.pickupLon);
|
|
1602
|
+
const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
|
|
1603
|
+
const notes = String(p.notes || '').trim();
|
|
1604
|
+
const addrHtml = addr
|
|
1605
|
+
? (hasCoords
|
|
1606
|
+
? `<a href="#" class="onekite-map-link" data-address="${escapeHtml(addr)}" data-lat="${escapeHtml(String(lat))}" data-lon="${escapeHtml(String(lon))}">${escapeHtml(addr)}</a>`
|
|
1607
|
+
: `${escapeHtml(addr)}`)
|
|
1608
|
+
: '';
|
|
1609
|
+
const html = `
|
|
1610
|
+
<div class="mb-2"><strong>Titre</strong><br>${escapeHtml(p.title || ev.title || '')}</div>
|
|
1611
|
+
${userLine}
|
|
1612
|
+
<div class="mb-2"><strong>Période</strong><br>${escapeHtml(formatDtWithTime(ev.start))} → ${escapeHtml(formatDtWithTime(ev.end))}</div>
|
|
1613
|
+
${addr ? `<div class="mb-2"><strong>Adresse</strong><br>${addrHtml}</div>` : ''}
|
|
1614
|
+
${notes ? `<div class="mb-2"><strong>Notes</strong><br>${escapeHtml(notes).replace(/\n/g,'<br>')}</div>` : ''}
|
|
1615
|
+
`;
|
|
1616
|
+
const canDel = !!(p.canDeleteOuting);
|
|
1617
|
+
bootbox.dialog({
|
|
1618
|
+
title: 'Prévision de sortie',
|
|
1619
|
+
message: html,
|
|
1620
|
+
buttons: {
|
|
1621
|
+
close: { label: 'Fermer', className: 'btn-secondary' },
|
|
1622
|
+
...(canDel ? {
|
|
1623
|
+
del: {
|
|
1624
|
+
label: 'Annuler',
|
|
1625
|
+
className: 'btn-danger',
|
|
1626
|
+
callback: async () => {
|
|
1627
|
+
try {
|
|
1628
|
+
const oid = String(p.oid || ev.id).replace(/^outing:/, '');
|
|
1629
|
+
await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}`, { method: 'DELETE' });
|
|
1630
|
+
showAlert('success', 'Prévision annulée.');
|
|
1631
|
+
calendar.refetchEvents();
|
|
1632
|
+
} catch (e) {
|
|
1633
|
+
showAlert('error', 'Annulation impossible.');
|
|
1634
|
+
}
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
} : {}),
|
|
1638
|
+
},
|
|
1639
|
+
});
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
// ignore
|
|
1707
1644
|
}
|
|
1645
|
+
|
|
1708
1646
|
const rid = p.rid || ev.id;
|
|
1709
1647
|
const status = p.status || '';
|
|
1710
1648
|
|
|
@@ -2016,9 +1954,6 @@ function toDatetimeLocalValue(date) {
|
|
|
2016
1954
|
message: baseHtml,
|
|
2017
1955
|
buttons,
|
|
2018
1956
|
});
|
|
2019
|
-
} finally {
|
|
2020
|
-
isDialogOpen = false;
|
|
2021
|
-
}
|
|
2022
1957
|
},
|
|
2023
1958
|
});
|
|
2024
1959
|
|
|
@@ -2150,7 +2085,7 @@ function toDatetimeLocalValue(date) {
|
|
|
2150
2085
|
}
|
|
2151
2086
|
|
|
2152
2087
|
|
|
2153
|
-
async function openFabDatePicker() {
|
|
2088
|
+
async function openFabDatePicker(onSelection) {
|
|
2154
2089
|
if (!lockAction('fab-date-picker', 700)) return;
|
|
2155
2090
|
|
|
2156
2091
|
// Cannot book past dates (mobile FAB). Same-day booking is allowed.
|
|
@@ -2208,44 +2143,16 @@ async function openFabDatePicker() {
|
|
|
2208
2143
|
const endExcl = new Date(e);
|
|
2209
2144
|
endExcl.setDate(endExcl.getDate() + 1);
|
|
2210
2145
|
|
|
2146
|
+
// If a callback is provided, delegate the next step (chooser).
|
|
2147
|
+
if (typeof onSelection === 'function') {
|
|
2148
|
+
try { onSelection({ start: s, end: endExcl, allDay: true }); } catch (e) {}
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2211
2152
|
(async () => {
|
|
2212
2153
|
try {
|
|
2213
2154
|
if (isDialogOpen) return;
|
|
2214
2155
|
isDialogOpen = true;
|
|
2215
|
-
|
|
2216
|
-
// Let the user pick between a Location or an Outing forecast.
|
|
2217
|
-
const choice = await new Promise((resolve) => {
|
|
2218
|
-
bootbox.dialog({
|
|
2219
|
-
title: 'Créer',
|
|
2220
|
-
message: '<div>Que veux-tu créer ?</div>',
|
|
2221
|
-
buttons: {
|
|
2222
|
-
cancel: { label: 'Annuler', className: 'btn-secondary', callback: () => resolve(null) },
|
|
2223
|
-
outing: { label: 'Prévision de sortie', className: 'btn-info', callback: () => resolve('outing') },
|
|
2224
|
-
reservation: { label: 'Location', className: 'btn-primary', callback: () => resolve('reservation') },
|
|
2225
|
-
},
|
|
2226
|
-
});
|
|
2227
|
-
});
|
|
2228
|
-
|
|
2229
|
-
if (!choice) return;
|
|
2230
|
-
|
|
2231
|
-
if (choice === 'outing') {
|
|
2232
|
-
const payload = await openSpecialEventDialog({ start: s, end: endExcl, allDay: true }, { kind: 'outing', dialogTitle: 'Créer une prévision de sortie', defaultTitle: 'Sortie' });
|
|
2233
|
-
if (!payload) return;
|
|
2234
|
-
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
2235
|
-
method: 'POST',
|
|
2236
|
-
body: JSON.stringify(payload),
|
|
2237
|
-
}).catch(async () => {
|
|
2238
|
-
return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
2239
|
-
method: 'POST',
|
|
2240
|
-
body: JSON.stringify(payload),
|
|
2241
|
-
});
|
|
2242
|
-
});
|
|
2243
|
-
showAlert('success', 'Sortie créée.');
|
|
2244
|
-
invalidateEventsCache();
|
|
2245
|
-
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2246
|
-
return;
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
2156
|
const items = cachedItems || (await loadItems());
|
|
2250
2157
|
const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
|
|
2251
2158
|
if (chosen && chosen.itemIds && chosen.itemIds.length) {
|
|
@@ -2268,8 +2175,6 @@ async function openFabDatePicker() {
|
|
|
2268
2175
|
}
|
|
2269
2176
|
} catch (err) {
|
|
2270
2177
|
// ignore
|
|
2271
|
-
} finally {
|
|
2272
|
-
isDialogOpen = false;
|
|
2273
2178
|
}
|
|
2274
2179
|
})();
|
|
2275
2180
|
|
|
@@ -2405,6 +2310,33 @@ async function openFabDatePicker() {
|
|
|
2405
2310
|
render();
|
|
2406
2311
|
});
|
|
2407
2312
|
}
|
|
2313
|
+
|
|
2314
|
+
// Swipe left/right to change month
|
|
2315
|
+
try {
|
|
2316
|
+
const root = document.getElementById('onekite-range-picker');
|
|
2317
|
+
if (root) {
|
|
2318
|
+
let x0 = null;
|
|
2319
|
+
root.addEventListener('touchstart', (ev) => {
|
|
2320
|
+
try { x0 = ev.touches && ev.touches[0] ? ev.touches[0].clientX : null; } catch (e) { x0 = null; }
|
|
2321
|
+
}, { passive: true });
|
|
2322
|
+
root.addEventListener('touchend', (ev) => {
|
|
2323
|
+
try {
|
|
2324
|
+
if (x0 === null) return;
|
|
2325
|
+
const x1 = ev.changedTouches && ev.changedTouches[0] ? ev.changedTouches[0].clientX : null;
|
|
2326
|
+
if (x1 === null) return;
|
|
2327
|
+
const dx = x1 - x0;
|
|
2328
|
+
if (Math.abs(dx) < 40) return;
|
|
2329
|
+
if (dx < 0) {
|
|
2330
|
+
state.cursor.setMonth(state.cursor.getMonth() + 1);
|
|
2331
|
+
} else {
|
|
2332
|
+
state.cursor.setMonth(state.cursor.getMonth() - 1);
|
|
2333
|
+
}
|
|
2334
|
+
render();
|
|
2335
|
+
} catch (e) {}
|
|
2336
|
+
finally { x0 = null; }
|
|
2337
|
+
}, { passive: true });
|
|
2338
|
+
}
|
|
2339
|
+
} catch (e) {}
|
|
2408
2340
|
render();
|
|
2409
2341
|
} catch (e) {}
|
|
2410
2342
|
});
|
|
@@ -2436,7 +2368,7 @@ function parseYmdDate(ymdStr) {
|
|
|
2436
2368
|
fabEl.setAttribute('aria-label', 'Nouvelle réservation');
|
|
2437
2369
|
fabEl.innerHTML = '<i class="fa fa-plus"></i>';
|
|
2438
2370
|
|
|
2439
|
-
fabHandler = () => openFabDatePicker();
|
|
2371
|
+
fabHandler = () => openFabDatePicker(handleCreateFromSelection);
|
|
2440
2372
|
fabEl.addEventListener('click', fabHandler);
|
|
2441
2373
|
|
|
2442
2374
|
document.body.appendChild(fabEl);
|