nodebb-plugin-onekite-calendar 2.0.48 → 2.0.50
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/api.js +16 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +350 -212
package/lib/api.js
CHANGED
|
@@ -924,7 +924,9 @@ api.getCapabilities = async function (req, res) {
|
|
|
924
924
|
canModerate: canMod,
|
|
925
925
|
canCreateSpecial: uid ? await canCreateSpecial(uid, settings) : false,
|
|
926
926
|
canDeleteSpecial: uid ? await canDeleteSpecial(uid, settings) : false,
|
|
927
|
+
// Outings share the same rights as reservations/locations.
|
|
927
928
|
canCreateOuting: uid ? await canRequest(uid, settings, Date.now()) : false,
|
|
929
|
+
canCreateReservation: uid ? await canRequest(uid, settings, Date.now()) : false,
|
|
928
930
|
});
|
|
929
931
|
};
|
|
930
932
|
|
|
@@ -1055,7 +1057,13 @@ api.createOuting = async function (req, res) {
|
|
|
1055
1057
|
// not on the outing date, so members can plan future outings without
|
|
1056
1058
|
// requiring next-year group membership.
|
|
1057
1059
|
const ok = await canRequest(req.uid, settings, Date.now());
|
|
1058
|
-
if (!ok)
|
|
1060
|
+
if (!ok) {
|
|
1061
|
+
return res.status(403).json({
|
|
1062
|
+
error: 'not-allowed',
|
|
1063
|
+
code: 'NOT_MEMBER',
|
|
1064
|
+
message: 'Vous devez être adhérent Onekite',
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1059
1067
|
|
|
1060
1068
|
const title = String((req.body && req.body.title) || '').trim() || 'Sortie';
|
|
1061
1069
|
const endTs = toTs(req.body && req.body.end);
|
|
@@ -1186,7 +1194,13 @@ api.createReservation = async function (req, res) {
|
|
|
1186
1194
|
const settings = await meta.settings.get('calendar-onekite');
|
|
1187
1195
|
const startPreview = toTs(req.body.start);
|
|
1188
1196
|
const ok = await canRequest(uid, settings, startPreview);
|
|
1189
|
-
if (!ok)
|
|
1197
|
+
if (!ok) {
|
|
1198
|
+
return res.status(403).json({
|
|
1199
|
+
error: 'not-allowed',
|
|
1200
|
+
code: 'NOT_MEMBER',
|
|
1201
|
+
message: 'Vous devez être adhérent Onekite',
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1190
1204
|
|
|
1191
1205
|
const isValidator = await canValidate(uid, settings);
|
|
1192
1206
|
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -134,6 +134,8 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
134
134
|
// calendar instance and capability flags). The mobile FAB is mounted outside
|
|
135
135
|
// init(), so we store the handler here.
|
|
136
136
|
let createFromSelectionHandler = null;
|
|
137
|
+
// Unified creation wizard (date range + type) used by the mobile FAB.
|
|
138
|
+
let openCreateWizardHandler = null;
|
|
137
139
|
|
|
138
140
|
// Mobile FAB (mounted only on the calendar page)
|
|
139
141
|
let fabEl = null;
|
|
@@ -1203,6 +1205,8 @@ function toDatetimeLocalValue(date) {
|
|
|
1203
1205
|
const caps = await loadCapabilities().catch(() => ({}));
|
|
1204
1206
|
const canCreateSpecial = !!caps.canCreateSpecial;
|
|
1205
1207
|
const canDeleteSpecial = !!caps.canDeleteSpecial;
|
|
1208
|
+
const canCreateOuting = !!caps.canCreateOuting;
|
|
1209
|
+
const canCreateReservation = !!caps.canCreateReservation;
|
|
1206
1210
|
|
|
1207
1211
|
// Creation chooser: Location / Prévision de sortie / Évènement (si autorisé).
|
|
1208
1212
|
|
|
@@ -1281,6 +1285,17 @@ function toDatetimeLocalValue(date) {
|
|
|
1281
1285
|
if (isDialogOpen) return;
|
|
1282
1286
|
if (!lockAction('create', 900)) return;
|
|
1283
1287
|
|
|
1288
|
+
// If the user is not an Onekite member (or not logged in), do not open
|
|
1289
|
+
// the creation chooser at all. This avoids a confusing "not-allowed"
|
|
1290
|
+
// error after the user fills the form.
|
|
1291
|
+
try {
|
|
1292
|
+
if (!canCreateReservation && !canCreateOuting && !canCreateSpecial) {
|
|
1293
|
+
showAlert('error', 'Vous devez être adhérent Onekite');
|
|
1294
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
} catch (e) {}
|
|
1298
|
+
|
|
1284
1299
|
// Business rule: nothing can be created in the past.
|
|
1285
1300
|
try {
|
|
1286
1301
|
const startDateCheck = toLocalYmd(info.start);
|
|
@@ -1322,116 +1337,162 @@ function toDatetimeLocalValue(date) {
|
|
|
1322
1337
|
}
|
|
1323
1338
|
}
|
|
1324
1339
|
|
|
1325
|
-
//
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const startDate = toLocalYmd(info.start);
|
|
1341
|
-
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(info.end);
|
|
1342
|
-
const resp = await requestReservation({
|
|
1343
|
-
title: (chosen && chosen.title) ? String(chosen.title) : '',
|
|
1344
|
-
start: startDate,
|
|
1345
|
-
end: endDate,
|
|
1346
|
-
itemIds: chosen.itemIds,
|
|
1347
|
-
itemNames: chosen.itemNames,
|
|
1348
|
-
total: chosen.total,
|
|
1349
|
-
});
|
|
1350
|
-
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1351
|
-
showAlert('success', 'Réservation confirmée.');
|
|
1352
|
-
} else {
|
|
1353
|
-
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
1354
|
-
}
|
|
1355
|
-
invalidateEventsCache();
|
|
1356
|
-
scheduleRefetch(calendar);
|
|
1357
|
-
} catch (e) {
|
|
1358
|
-
handleCreateError(e);
|
|
1359
|
-
} finally {
|
|
1360
|
-
try { calendar.unselect(); } catch (e) {}
|
|
1361
|
-
isDialogOpen = false;
|
|
1340
|
+
// Single entry modal: pick date(s) + choose type (FAB & calendar),
|
|
1341
|
+
// then open the dedicated form dialog.
|
|
1342
|
+
await openCreateWizardModal({
|
|
1343
|
+
initialSelection: info,
|
|
1344
|
+
// When the user clicks/selects on the main calendar, we already have
|
|
1345
|
+
// the date(s) and should not show the mini date picker (FAB only).
|
|
1346
|
+
showDatePicker: false,
|
|
1347
|
+
canCreateReservation,
|
|
1348
|
+
canCreateOuting,
|
|
1349
|
+
canCreateSpecial,
|
|
1350
|
+
onCreateLocation: async (sel) => {
|
|
1351
|
+
try {
|
|
1352
|
+
if (!items || !items.length) {
|
|
1353
|
+
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1354
|
+
return;
|
|
1362
1355
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
showAlert('success', 'Prévision de sortie créée.');
|
|
1380
|
-
invalidateEventsCache();
|
|
1381
|
-
scheduleRefetch(calendar);
|
|
1382
|
-
} catch (e) {
|
|
1383
|
-
handleCreateError(e);
|
|
1384
|
-
} finally {
|
|
1385
|
-
try { calendar.unselect(); } catch (e) {}
|
|
1386
|
-
isDialogOpen = false;
|
|
1356
|
+
const chosen = await openReservationDialog(sel, items);
|
|
1357
|
+
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
|
|
1358
|
+
const startDate = toLocalYmd(sel.start);
|
|
1359
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(sel.end);
|
|
1360
|
+
const resp = await requestReservation({
|
|
1361
|
+
title: (chosen && chosen.title) ? String(chosen.title) : '',
|
|
1362
|
+
start: startDate,
|
|
1363
|
+
end: endDate,
|
|
1364
|
+
itemIds: chosen.itemIds,
|
|
1365
|
+
itemNames: chosen.itemNames,
|
|
1366
|
+
total: chosen.total,
|
|
1367
|
+
});
|
|
1368
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1369
|
+
showAlert('success', 'Réservation confirmée.');
|
|
1370
|
+
} else {
|
|
1371
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
1387
1372
|
}
|
|
1388
|
-
|
|
1373
|
+
invalidateEventsCache();
|
|
1374
|
+
scheduleRefetch(calendar);
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
handleCreateError(e);
|
|
1377
|
+
} finally {
|
|
1378
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
onCreateOuting: async (sel) => {
|
|
1382
|
+
try {
|
|
1383
|
+
const payload = await openOutingDialog(sel);
|
|
1384
|
+
if (!payload) return;
|
|
1385
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/outings', {
|
|
1386
|
+
method: 'POST',
|
|
1387
|
+
body: JSON.stringify(payload),
|
|
1388
|
+
});
|
|
1389
|
+
showAlert('success', 'Prévision de sortie créée.');
|
|
1390
|
+
invalidateEventsCache();
|
|
1391
|
+
scheduleRefetch(calendar);
|
|
1392
|
+
} catch (e) {
|
|
1393
|
+
handleCreateError(e);
|
|
1394
|
+
} finally {
|
|
1395
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
onCreateSpecial: async (sel) => {
|
|
1399
|
+
try {
|
|
1400
|
+
const payload = await openSpecialEventDialog(sel);
|
|
1401
|
+
if (!payload) return;
|
|
1402
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1403
|
+
method: 'POST',
|
|
1404
|
+
body: JSON.stringify(payload),
|
|
1405
|
+
});
|
|
1406
|
+
showAlert('success', 'Évènement créé.');
|
|
1407
|
+
invalidateEventsCache();
|
|
1408
|
+
scheduleRefetch(calendar);
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
handleCreateError(e);
|
|
1411
|
+
} finally {
|
|
1412
|
+
try { calendar.unselect(); } catch (e) {}
|
|
1413
|
+
}
|
|
1389
1414
|
},
|
|
1390
|
-
};
|
|
1391
|
-
|
|
1392
|
-
if (canCreateSpecial) {
|
|
1393
|
-
buttons.special = {
|
|
1394
|
-
label: 'Évènement',
|
|
1395
|
-
className: 'btn-onekite-special',
|
|
1396
|
-
callback: async () => {
|
|
1397
|
-
try {
|
|
1398
|
-
isDialogOpen = true;
|
|
1399
|
-
const payload = await openSpecialEventDialog(info);
|
|
1400
|
-
if (!payload) return;
|
|
1401
|
-
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1402
|
-
method: 'POST',
|
|
1403
|
-
body: JSON.stringify(payload),
|
|
1404
|
-
}).catch(async () => {
|
|
1405
|
-
return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1406
|
-
method: 'POST',
|
|
1407
|
-
body: JSON.stringify(payload),
|
|
1408
|
-
});
|
|
1409
|
-
});
|
|
1410
|
-
showAlert('success', 'Évènement créé.');
|
|
1411
|
-
invalidateEventsCache();
|
|
1412
|
-
scheduleRefetch(calendar);
|
|
1413
|
-
} catch (e) {
|
|
1414
|
-
handleCreateError(e);
|
|
1415
|
-
} finally {
|
|
1416
|
-
try { calendar.unselect(); } catch (e) {}
|
|
1417
|
-
isDialogOpen = false;
|
|
1418
|
-
}
|
|
1419
|
-
return true;
|
|
1420
|
-
},
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
buttons.cancel = { label: 'Annuler', className: 'btn-danger' };
|
|
1425
|
-
|
|
1426
|
-
bootbox.dialog({
|
|
1427
|
-
title: 'Créer',
|
|
1428
|
-
message: '<div class="text-muted">Que veux-tu créer sur ces dates ?</div>',
|
|
1429
|
-
buttons,
|
|
1430
1415
|
});
|
|
1431
1416
|
}
|
|
1432
1417
|
|
|
1433
1418
|
// Expose to the mobile FAB (mounted outside init).
|
|
1434
1419
|
createFromSelectionHandler = handleCreateFromSelection;
|
|
1420
|
+
openCreateWizardHandler = async () => {
|
|
1421
|
+
// Same pre-checks as calendar creation, but with no initial selection.
|
|
1422
|
+
if (!canCreateReservation && !canCreateOuting && !canCreateSpecial) {
|
|
1423
|
+
showAlert('error', 'Vous devez être adhérent Onekite');
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
await openCreateWizardModal({
|
|
1427
|
+
initialSelection: null,
|
|
1428
|
+
canCreateReservation,
|
|
1429
|
+
canCreateOuting,
|
|
1430
|
+
canCreateSpecial,
|
|
1431
|
+
onCreateLocation: async (sel) => {
|
|
1432
|
+
try {
|
|
1433
|
+
if (!items || !items.length) {
|
|
1434
|
+
showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const chosen = await openReservationDialog(sel, items);
|
|
1438
|
+
if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
|
|
1439
|
+
const startDate = toLocalYmd(sel.start);
|
|
1440
|
+
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(sel.end);
|
|
1441
|
+
const resp = await requestReservation({
|
|
1442
|
+
title: (chosen && chosen.title) ? String(chosen.title) : '',
|
|
1443
|
+
start: startDate,
|
|
1444
|
+
end: endDate,
|
|
1445
|
+
itemIds: chosen.itemIds,
|
|
1446
|
+
itemNames: chosen.itemNames,
|
|
1447
|
+
total: chosen.total,
|
|
1448
|
+
});
|
|
1449
|
+
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
1450
|
+
showAlert('success', 'Réservation confirmée.');
|
|
1451
|
+
} else {
|
|
1452
|
+
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
1453
|
+
}
|
|
1454
|
+
invalidateEventsCache();
|
|
1455
|
+
scheduleRefetch(calendar);
|
|
1456
|
+
} catch (e) {
|
|
1457
|
+
// Reuse calendar handler (simplified): show payload message when present
|
|
1458
|
+
const msg = (e && e.payload && (e.payload.message || e.payload.error || e.payload.msg)) ? String(e.payload.message || e.payload.error || e.payload.msg) : '';
|
|
1459
|
+
showAlert('error', msg || 'Erreur lors de la création.');
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
onCreateOuting: async (sel) => {
|
|
1463
|
+
try {
|
|
1464
|
+
const payload = await openOutingDialog(sel);
|
|
1465
|
+
if (!payload) return;
|
|
1466
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/outings', {
|
|
1467
|
+
method: 'POST',
|
|
1468
|
+
body: JSON.stringify(payload),
|
|
1469
|
+
});
|
|
1470
|
+
showAlert('success', 'Prévision de sortie créée.');
|
|
1471
|
+
invalidateEventsCache();
|
|
1472
|
+
scheduleRefetch(calendar);
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
const msg = (e && e.payload && (e.payload.message || e.payload.error || e.payload.msg)) ? String(e.payload.message || e.payload.error || e.payload.msg) : '';
|
|
1475
|
+
showAlert('error', msg || 'Erreur lors de la création.');
|
|
1476
|
+
}
|
|
1477
|
+
},
|
|
1478
|
+
onCreateSpecial: async (sel) => {
|
|
1479
|
+
try {
|
|
1480
|
+
const payload = await openSpecialEventDialog(sel);
|
|
1481
|
+
if (!payload) return;
|
|
1482
|
+
await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
|
|
1483
|
+
method: 'POST',
|
|
1484
|
+
body: JSON.stringify(payload),
|
|
1485
|
+
});
|
|
1486
|
+
showAlert('success', 'Évènement créé.');
|
|
1487
|
+
invalidateEventsCache();
|
|
1488
|
+
scheduleRefetch(calendar);
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
const msg = (e && e.payload && (e.payload.message || e.payload.error || e.payload.msg)) ? String(e.payload.message || e.payload.error || e.payload.msg) : '';
|
|
1491
|
+
showAlert('error', msg || 'Erreur lors de la création.');
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
};
|
|
1435
1496
|
|
|
1436
1497
|
calendar = new FullCalendar.Calendar(el, {
|
|
1437
1498
|
initialView: 'dayGridMonth',
|
|
@@ -2193,10 +2254,21 @@ function toDatetimeLocalValue(date) {
|
|
|
2193
2254
|
}
|
|
2194
2255
|
|
|
2195
2256
|
|
|
2196
|
-
async function
|
|
2197
|
-
if (!lockAction('
|
|
2257
|
+
async function openCreateWizardModal(opts) {
|
|
2258
|
+
if (!lockAction('create-wizard', 700)) return;
|
|
2198
2259
|
|
|
2199
|
-
|
|
2260
|
+
const options = opts || {};
|
|
2261
|
+
const canCreateReservation = !!options.canCreateReservation;
|
|
2262
|
+
const canCreateOuting = !!options.canCreateOuting;
|
|
2263
|
+
const canCreateSpecial = !!options.canCreateSpecial;
|
|
2264
|
+
|
|
2265
|
+
// Only the mobile FAB should show the mini range picker calendar.
|
|
2266
|
+
// For calendar interactions (date click / range select), we already know
|
|
2267
|
+
// the selected dates, so we can skip the mini calendar and jump straight
|
|
2268
|
+
// to the type chooser.
|
|
2269
|
+
const showDatePicker = options.showDatePicker !== false;
|
|
2270
|
+
|
|
2271
|
+
// Cannot create in the past (wizard). Same-day is allowed.
|
|
2200
2272
|
const today = new Date();
|
|
2201
2273
|
today.setHours(0, 0, 0, 0);
|
|
2202
2274
|
const minStart = new Date(today);
|
|
@@ -2212,86 +2284,89 @@ async function openFabDatePicker(onSelection) {
|
|
|
2212
2284
|
end: null, // inclusive
|
|
2213
2285
|
};
|
|
2214
2286
|
|
|
2215
|
-
|
|
2216
|
-
|
|
2287
|
+
// Bonus UX: once dates are picked, scroll the type chooser into view.
|
|
2288
|
+
let didAutoScrollToType = false;
|
|
2289
|
+
|
|
2290
|
+
// Pre-fill from an existing FullCalendar selection (end is exclusive)
|
|
2291
|
+
try {
|
|
2292
|
+
const initial = options.initialSelection;
|
|
2293
|
+
if (initial && initial.start) {
|
|
2294
|
+
const s = new Date(initial.start);
|
|
2295
|
+
s.setHours(0, 0, 0, 0);
|
|
2296
|
+
state.start = s;
|
|
2297
|
+
if (initial.end) {
|
|
2298
|
+
const eEx = new Date(initial.end);
|
|
2299
|
+
eEx.setHours(0, 0, 0, 0);
|
|
2300
|
+
// end inclusive = end exclusive - 1 day
|
|
2301
|
+
const e = new Date(eEx);
|
|
2302
|
+
e.setDate(e.getDate() - 1);
|
|
2303
|
+
if (e.getTime() >= s.getTime()) {
|
|
2304
|
+
state.end = e;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
state.cursor = new Date(s);
|
|
2308
|
+
state.cursor.setDate(1);
|
|
2309
|
+
}
|
|
2310
|
+
} catch (e) {}
|
|
2311
|
+
|
|
2312
|
+
function summaryText() {
|
|
2313
|
+
if (!state.start) return 'Aucune date sélectionnée.';
|
|
2314
|
+
const startTxt = formatDdMmYyyy(state.start);
|
|
2315
|
+
const endTxt = formatDdMmYyyy(state.end || state.start);
|
|
2316
|
+
return (state.end && !sameDay(state.start, state.end))
|
|
2317
|
+
? `Du ${startTxt} au ${endTxt}`
|
|
2318
|
+
: `Le ${startTxt}`;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const datePickerHtml = showDatePicker ? `
|
|
2217
2322
|
<div class="onekite-range-header">
|
|
2218
|
-
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-
|
|
2219
|
-
<div class="onekite-range-month" id="onekite-
|
|
2220
|
-
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-
|
|
2323
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-wiz-prev" aria-label="Mois précédent">‹</button>
|
|
2324
|
+
<div class="onekite-range-month" id="onekite-wiz-month"></div>
|
|
2325
|
+
<button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-wiz-next" aria-label="Mois suivant">›</button>
|
|
2221
2326
|
</div>
|
|
2222
2327
|
<div class="onekite-range-weekdays">
|
|
2223
2328
|
<div>L</div><div>M</div><div>M</div><div>J</div><div>V</div><div>S</div><div>D</div>
|
|
2224
2329
|
</div>
|
|
2225
|
-
<div class="onekite-range-grid" id="onekite-
|
|
2226
|
-
<div class="onekite-range-summary" id="onekite-
|
|
2330
|
+
<div class="onekite-range-grid" id="onekite-wiz-grid"></div>
|
|
2331
|
+
<div class="onekite-range-summary" id="onekite-wiz-summary"></div>
|
|
2227
2332
|
<div class="form-text mt-2">Sélectionne une date de début puis une date de fin (incluse). Les dates passées sont désactivées.</div>
|
|
2333
|
+
|
|
2334
|
+
<hr class="my-3" />
|
|
2335
|
+
` : `
|
|
2336
|
+
<div class="onekite-range-summary" id="onekite-wiz-summary-static"><strong>${escapeHtml(summaryText())}</strong></div>
|
|
2337
|
+
<hr class="my-3" />
|
|
2338
|
+
`;
|
|
2339
|
+
|
|
2340
|
+
const html = `
|
|
2341
|
+
<div class="onekite-range-picker" id="onekite-wiz-root">
|
|
2342
|
+
${datePickerHtml}
|
|
2343
|
+
|
|
2344
|
+
<div id="onekite-wiz-type" class="onekite-wiz-type">
|
|
2345
|
+
<div class="text-muted mb-2">Que veux-tu créer sur cette/ces date(s) ?</div>
|
|
2346
|
+
<div class="d-flex flex-wrap gap-2 justify-content-center">
|
|
2347
|
+
${canCreateReservation ? '<button type="button" class="btn btn-onekite-location" id="onekite-wiz-btn-location">Location</button>' : ''}
|
|
2348
|
+
${canCreateOuting ? '<button type="button" class="btn btn-onekite-outing" id="onekite-wiz-btn-outing">Prévision de sortie</button>' : ''}
|
|
2349
|
+
${canCreateSpecial ? '<button type="button" class="btn btn-onekite-special" id="onekite-wiz-btn-special">Évènement</button>' : ''}
|
|
2350
|
+
</div>
|
|
2351
|
+
<div class="form-text mt-2">Astuce : une fois les dates choisies, tu peux directement sélectionner le type ci-dessus.</div>
|
|
2352
|
+
</div>
|
|
2228
2353
|
</div>
|
|
2229
2354
|
`;
|
|
2230
2355
|
|
|
2231
2356
|
const dlg = bootbox.dialog({
|
|
2232
|
-
title: '
|
|
2357
|
+
title: 'Créer',
|
|
2233
2358
|
message: html,
|
|
2234
2359
|
buttons: {
|
|
2235
|
-
cancel: { label: 'Annuler', className: 'btn-
|
|
2236
|
-
ok: {
|
|
2237
|
-
label: 'Continuer',
|
|
2238
|
-
className: 'btn-primary',
|
|
2239
|
-
callback: function () {
|
|
2240
|
-
const s = state.start;
|
|
2241
|
-
const e = state.end || state.start;
|
|
2242
|
-
if (!s) {
|
|
2243
|
-
alerts.error('Choisis une date de début.');
|
|
2244
|
-
return false;
|
|
2245
|
-
}
|
|
2246
|
-
if (!e) {
|
|
2247
|
-
alerts.error('Choisis une date de fin.');
|
|
2248
|
-
return false;
|
|
2249
|
-
}
|
|
2250
|
-
// Convert end inclusive -> end exclusive (FullCalendar rule)
|
|
2251
|
-
const endExcl = new Date(e);
|
|
2252
|
-
endExcl.setDate(endExcl.getDate() + 1);
|
|
2253
|
-
|
|
2254
|
-
// If a callback is provided, delegate the next step (chooser).
|
|
2255
|
-
if (typeof onSelection === 'function') {
|
|
2256
|
-
try { onSelection({ start: s, end: endExcl, allDay: true }); } catch (e) {}
|
|
2257
|
-
return true;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
(async () => {
|
|
2261
|
-
try {
|
|
2262
|
-
if (isDialogOpen) return;
|
|
2263
|
-
isDialogOpen = true;
|
|
2264
|
-
const items = cachedItems || (await loadItems());
|
|
2265
|
-
const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
|
|
2266
|
-
if (chosen && chosen.itemIds && chosen.itemIds.length) {
|
|
2267
|
-
const startDate = toLocalYmd(s);
|
|
2268
|
-
const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(endExcl);
|
|
2269
|
-
const resp = await requestReservation({
|
|
2270
|
-
start: startDate,
|
|
2271
|
-
end: endDate,
|
|
2272
|
-
itemIds: chosen.itemIds,
|
|
2273
|
-
itemNames: chosen.itemNames,
|
|
2274
|
-
total: chosen.total,
|
|
2275
|
-
});
|
|
2276
|
-
if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
|
|
2277
|
-
showAlert('success', 'Réservation confirmée.');
|
|
2278
|
-
} else {
|
|
2279
|
-
showAlert('success', 'Demande envoyée (en attente de validation).');
|
|
2280
|
-
}
|
|
2281
|
-
invalidateEventsCache();
|
|
2282
|
-
if (currentCalendar) scheduleRefetch(currentCalendar);
|
|
2283
|
-
}
|
|
2284
|
-
} catch (err) {
|
|
2285
|
-
// ignore
|
|
2286
|
-
}
|
|
2287
|
-
})();
|
|
2288
|
-
|
|
2289
|
-
return true;
|
|
2290
|
-
},
|
|
2291
|
-
},
|
|
2360
|
+
cancel: { label: 'Annuler', className: 'btn-danger' },
|
|
2292
2361
|
},
|
|
2293
2362
|
});
|
|
2294
2363
|
|
|
2364
|
+
// Mark as open to prevent duplicate modals (select/dateClick on mobile)
|
|
2365
|
+
isDialogOpen = true;
|
|
2366
|
+
try {
|
|
2367
|
+
dlg.on('hidden.bs.modal', () => { isDialogOpen = false; });
|
|
2368
|
+
} catch (e) {}
|
|
2369
|
+
|
|
2295
2370
|
function sameDay(a, b) {
|
|
2296
2371
|
return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
2297
2372
|
}
|
|
@@ -2311,9 +2386,10 @@ async function openFabDatePicker(onSelection) {
|
|
|
2311
2386
|
}
|
|
2312
2387
|
|
|
2313
2388
|
function render() {
|
|
2314
|
-
|
|
2315
|
-
const
|
|
2316
|
-
const
|
|
2389
|
+
if (!showDatePicker) return;
|
|
2390
|
+
const monthEl = document.getElementById('onekite-wiz-month');
|
|
2391
|
+
const gridEl = document.getElementById('onekite-wiz-grid');
|
|
2392
|
+
const sumEl = document.getElementById('onekite-wiz-summary');
|
|
2317
2393
|
if (!monthEl || !gridEl || !sumEl) return;
|
|
2318
2394
|
|
|
2319
2395
|
monthEl.textContent = monthLabel(state.cursor);
|
|
@@ -2373,6 +2449,12 @@ async function openFabDatePicker(onSelection) {
|
|
|
2373
2449
|
state.end = null;
|
|
2374
2450
|
// If cursor month is before selected month, keep cursor
|
|
2375
2451
|
render();
|
|
2452
|
+
// Bonus UX: once a start date is chosen, bring the type buttons
|
|
2453
|
+
// into view so the user can continue immediately (only once).
|
|
2454
|
+
if (!didAutoScrollToType) {
|
|
2455
|
+
didAutoScrollToType = true;
|
|
2456
|
+
try { document.getElementById('onekite-wiz-type')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
|
|
2457
|
+
}
|
|
2376
2458
|
return;
|
|
2377
2459
|
}
|
|
2378
2460
|
// Second click: set end (inclusive)
|
|
@@ -2384,6 +2466,10 @@ async function openFabDatePicker(onSelection) {
|
|
|
2384
2466
|
}
|
|
2385
2467
|
state.end = clicked;
|
|
2386
2468
|
render();
|
|
2469
|
+
if (!didAutoScrollToType) {
|
|
2470
|
+
didAutoScrollToType = true;
|
|
2471
|
+
try { document.getElementById('onekite-wiz-type')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
|
|
2472
|
+
}
|
|
2387
2473
|
});
|
|
2388
2474
|
}
|
|
2389
2475
|
|
|
@@ -2404,48 +2490,100 @@ async function openFabDatePicker(onSelection) {
|
|
|
2404
2490
|
|
|
2405
2491
|
dlg.on('shown.bs.modal', () => {
|
|
2406
2492
|
try {
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2493
|
+
if (showDatePicker) {
|
|
2494
|
+
const prevBtn = document.getElementById('onekite-wiz-prev');
|
|
2495
|
+
const nextBtn = document.getElementById('onekite-wiz-next');
|
|
2496
|
+
if (prevBtn) {
|
|
2497
|
+
prevBtn.addEventListener('click', () => {
|
|
2498
|
+
state.cursor.setMonth(state.cursor.getMonth() - 1);
|
|
2499
|
+
render();
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
if (nextBtn) {
|
|
2503
|
+
nextBtn.addEventListener('click', () => {
|
|
2504
|
+
state.cursor.setMonth(state.cursor.getMonth() + 1);
|
|
2505
|
+
render();
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// Swipe left/right to change month
|
|
2510
|
+
try {
|
|
2511
|
+
const root = document.getElementById('onekite-wiz-root');
|
|
2512
|
+
if (root) {
|
|
2513
|
+
let x0 = null;
|
|
2514
|
+
root.addEventListener('touchstart', (ev) => {
|
|
2515
|
+
try { x0 = ev.touches && ev.touches[0] ? ev.touches && ev.touches[0] ? ev.touches[0].clientX : null : null; } catch (e) { x0 = null; }
|
|
2516
|
+
}, { passive: true });
|
|
2517
|
+
root.addEventListener('touchend', (ev) => {
|
|
2518
|
+
try {
|
|
2519
|
+
if (x0 === null) return;
|
|
2520
|
+
const x1 = ev.changedTouches && ev.changedTouches[0] ? ev.changedTouches[0].clientX : null;
|
|
2521
|
+
if (x1 === null) return;
|
|
2522
|
+
const dx = x1 - x0;
|
|
2523
|
+
if (Math.abs(dx) < 40) return;
|
|
2524
|
+
if (dx < 0) {
|
|
2525
|
+
state.cursor.setMonth(state.cursor.getMonth() + 1);
|
|
2526
|
+
} else {
|
|
2527
|
+
state.cursor.setMonth(state.cursor.getMonth() - 1);
|
|
2528
|
+
}
|
|
2529
|
+
render();
|
|
2530
|
+
} catch (e) {}
|
|
2531
|
+
finally { x0 = null; }
|
|
2532
|
+
}, { passive: true });
|
|
2533
|
+
}
|
|
2534
|
+
} catch (e) {}
|
|
2535
|
+
} else {
|
|
2536
|
+
// Keep the static summary in sync (pre-filled by calendar click/selection).
|
|
2537
|
+
try {
|
|
2538
|
+
const el = document.getElementById('onekite-wiz-summary-static');
|
|
2539
|
+
if (el) el.innerHTML = `<strong>${escapeHtml(summaryText())}</strong>`;
|
|
2540
|
+
} catch (e) {}
|
|
2414
2541
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2542
|
+
|
|
2543
|
+
// Wire type buttons
|
|
2544
|
+
function ensureDatesOrToast() {
|
|
2545
|
+
if (!state.start) {
|
|
2546
|
+
showAlert('error', 'Choisis d\'abord une date.');
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
const s = new Date(state.start);
|
|
2550
|
+
const eIncl = state.end || state.start;
|
|
2551
|
+
const endExcl = new Date(eIncl);
|
|
2552
|
+
endExcl.setDate(endExcl.getDate() + 1);
|
|
2553
|
+
return { start: s, end: endExcl, allDay: true };
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
async function run(kind) {
|
|
2557
|
+
const sel = ensureDatesOrToast();
|
|
2558
|
+
if (!sel) return;
|
|
2559
|
+
try { dlg.modal('hide'); } catch (e) { try { bootbox.hideAll(); } catch (e2) {} }
|
|
2560
|
+
|
|
2561
|
+
// Delegate to the existing flows (forms) while keeping the wizard
|
|
2562
|
+
// as the single entry-point for date+type selection.
|
|
2563
|
+
if (kind === 'location' && typeof options.onCreateLocation === 'function') {
|
|
2564
|
+
await options.onCreateLocation(sel);
|
|
2565
|
+
} else if (kind === 'outing' && typeof options.onCreateOuting === 'function') {
|
|
2566
|
+
await options.onCreateOuting(sel);
|
|
2567
|
+
} else if (kind === 'special' && typeof options.onCreateSpecial === 'function') {
|
|
2568
|
+
await options.onCreateSpecial(sel);
|
|
2569
|
+
}
|
|
2420
2570
|
}
|
|
2421
2571
|
|
|
2422
|
-
|
|
2572
|
+
document.getElementById('onekite-wiz-btn-location')?.addEventListener('click', () => { run('location'); });
|
|
2573
|
+
document.getElementById('onekite-wiz-btn-outing')?.addEventListener('click', () => { run('outing'); });
|
|
2574
|
+
document.getElementById('onekite-wiz-btn-special')?.addEventListener('click', () => { run('special'); });
|
|
2575
|
+
|
|
2576
|
+
render();
|
|
2577
|
+
|
|
2578
|
+
// Bonus UX: if dates are already pre-filled (calendar click/selection),
|
|
2579
|
+
// gently scroll to the type selector.
|
|
2423
2580
|
try {
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
try { x0 = ev.touches && ev.touches[0] ? ev.touches[0].clientX : null; } catch (e) { x0 = null; }
|
|
2429
|
-
}, { passive: true });
|
|
2430
|
-
root.addEventListener('touchend', (ev) => {
|
|
2431
|
-
try {
|
|
2432
|
-
if (x0 === null) return;
|
|
2433
|
-
const x1 = ev.changedTouches && ev.changedTouches[0] ? ev.changedTouches[0].clientX : null;
|
|
2434
|
-
if (x1 === null) return;
|
|
2435
|
-
const dx = x1 - x0;
|
|
2436
|
-
if (Math.abs(dx) < 40) return;
|
|
2437
|
-
if (dx < 0) {
|
|
2438
|
-
state.cursor.setMonth(state.cursor.getMonth() + 1);
|
|
2439
|
-
} else {
|
|
2440
|
-
state.cursor.setMonth(state.cursor.getMonth() - 1);
|
|
2441
|
-
}
|
|
2442
|
-
render();
|
|
2443
|
-
} catch (e) {}
|
|
2444
|
-
finally { x0 = null; }
|
|
2445
|
-
}, { passive: true });
|
|
2581
|
+
if (state.start) {
|
|
2582
|
+
setTimeout(() => {
|
|
2583
|
+
try { document.getElementById('onekite-wiz-type')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
|
|
2584
|
+
}, 150);
|
|
2446
2585
|
}
|
|
2447
2586
|
} catch (e) {}
|
|
2448
|
-
render();
|
|
2449
2587
|
} catch (e) {}
|
|
2450
2588
|
});
|
|
2451
2589
|
}
|
|
@@ -2477,10 +2615,10 @@ function parseYmdDate(ymdStr) {
|
|
|
2477
2615
|
fabEl.innerHTML = '<i class="fa fa-plus"></i>';
|
|
2478
2616
|
|
|
2479
2617
|
fabHandler = () => {
|
|
2480
|
-
// init() sets
|
|
2618
|
+
// init() sets openCreateWizardHandler. If the calendar has not
|
|
2481
2619
|
// finished initialising, do nothing.
|
|
2482
|
-
if (typeof
|
|
2483
|
-
|
|
2620
|
+
if (typeof openCreateWizardHandler !== 'function') return;
|
|
2621
|
+
openCreateWizardHandler();
|
|
2484
2622
|
};
|
|
2485
2623
|
fabEl.addEventListener('click', fabHandler);
|
|
2486
2624
|
|