nodebb-plugin-onekite-calendar 2.0.49 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.49",
3
+ "version": "2.0.50",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "2.0.49"
42
+ "version": "2.0.50"
43
43
  }
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;
@@ -1335,116 +1337,162 @@ function toDatetimeLocalValue(date) {
1335
1337
  }
1336
1338
  }
1337
1339
 
1338
- // Buttons order matters for UX: put "Annuler" at bottom-right (last).
1339
- const buttons = {};
1340
-
1341
- if (canCreateReservation) buttons.location = {
1342
- label: 'Location',
1343
- className: 'btn-onekite-location',
1344
- callback: async () => {
1345
- try {
1346
- isDialogOpen = true;
1347
- if (!items || !items.length) {
1348
- showAlert('error', 'Aucun matériel disponible (items HelloAsso non chargés).');
1349
- return;
1350
- }
1351
- const chosen = await openReservationDialog(info, items);
1352
- if (!chosen || !chosen.itemIds || !chosen.itemIds.length) return;
1353
- const startDate = toLocalYmd(info.start);
1354
- const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(info.end);
1355
- const resp = await requestReservation({
1356
- title: (chosen && chosen.title) ? String(chosen.title) : '',
1357
- start: startDate,
1358
- end: endDate,
1359
- itemIds: chosen.itemIds,
1360
- itemNames: chosen.itemNames,
1361
- total: chosen.total,
1362
- });
1363
- if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
1364
- showAlert('success', 'Réservation confirmée.');
1365
- } else {
1366
- showAlert('success', 'Demande envoyée (en attente de validation).');
1367
- }
1368
- invalidateEventsCache();
1369
- scheduleRefetch(calendar);
1370
- } catch (e) {
1371
- handleCreateError(e);
1372
- } finally {
1373
- try { calendar.unselect(); } catch (e) {}
1374
- 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;
1375
1355
  }
1376
- return true;
1377
- },
1378
- };
1379
-
1380
- if (canCreateOuting) buttons.outing = {
1381
- label: 'Prévision de sortie',
1382
- className: 'btn-onekite-outing',
1383
- callback: async () => {
1384
- try {
1385
- isDialogOpen = true;
1386
- const payload = await openOutingDialog(info);
1387
- if (!payload) return;
1388
- await fetchJson('/api/v3/plugins/calendar-onekite/outings', {
1389
- method: 'POST',
1390
- body: JSON.stringify(payload),
1391
- });
1392
- showAlert('success', 'Prévision de sortie créée.');
1393
- invalidateEventsCache();
1394
- scheduleRefetch(calendar);
1395
- } catch (e) {
1396
- handleCreateError(e);
1397
- } finally {
1398
- try { calendar.unselect(); } catch (e) {}
1399
- 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).');
1400
1372
  }
1401
- return true;
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
+ }
1402
1414
  },
1403
- };
1404
-
1405
- if (canCreateSpecial) {
1406
- buttons.special = {
1407
- label: 'Évènement',
1408
- className: 'btn-onekite-special',
1409
- callback: async () => {
1410
- try {
1411
- isDialogOpen = true;
1412
- const payload = await openSpecialEventDialog(info);
1413
- if (!payload) return;
1414
- await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1415
- method: 'POST',
1416
- body: JSON.stringify(payload),
1417
- }).catch(async () => {
1418
- return await fetchJson('/api/v3/plugins/calendar-onekite/special-events', {
1419
- method: 'POST',
1420
- body: JSON.stringify(payload),
1421
- });
1422
- });
1423
- showAlert('success', 'Évènement créé.');
1424
- invalidateEventsCache();
1425
- scheduleRefetch(calendar);
1426
- } catch (e) {
1427
- handleCreateError(e);
1428
- } finally {
1429
- try { calendar.unselect(); } catch (e) {}
1430
- isDialogOpen = false;
1431
- }
1432
- return true;
1433
- },
1434
- };
1435
- }
1436
-
1437
- buttons.cancel = { label: 'Annuler', className: 'btn-danger' };
1438
-
1439
- bootbox.dialog({
1440
- title: 'Créer',
1441
- message: '<div class="text-muted">Que veux-tu créer sur ces dates ?</div>',
1442
- buttons,
1443
1415
  });
1444
1416
  }
1445
1417
 
1446
1418
  // Expose to the mobile FAB (mounted outside init).
1447
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
+ };
1448
1496
 
1449
1497
  calendar = new FullCalendar.Calendar(el, {
1450
1498
  initialView: 'dayGridMonth',
@@ -2206,10 +2254,21 @@ function toDatetimeLocalValue(date) {
2206
2254
  }
2207
2255
 
2208
2256
 
2209
- async function openFabDatePicker(onSelection) {
2210
- if (!lockAction('fab-date-picker', 700)) return;
2257
+ async function openCreateWizardModal(opts) {
2258
+ if (!lockAction('create-wizard', 700)) return;
2259
+
2260
+ const options = opts || {};
2261
+ const canCreateReservation = !!options.canCreateReservation;
2262
+ const canCreateOuting = !!options.canCreateOuting;
2263
+ const canCreateSpecial = !!options.canCreateSpecial;
2211
2264
 
2212
- // Cannot book past dates (mobile FAB). Same-day booking is allowed.
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.
2213
2272
  const today = new Date();
2214
2273
  today.setHours(0, 0, 0, 0);
2215
2274
  const minStart = new Date(today);
@@ -2225,86 +2284,89 @@ async function openFabDatePicker(onSelection) {
2225
2284
  end: null, // inclusive
2226
2285
  };
2227
2286
 
2228
- const html = `
2229
- <div class="onekite-range-picker" id="onekite-range-picker">
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 ? `
2230
2322
  <div class="onekite-range-header">
2231
- <button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-prev" aria-label="Mois précédent">&lsaquo;</button>
2232
- <div class="onekite-range-month" id="onekite-range-month"></div>
2233
- <button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-range-next" aria-label="Mois suivant">&rsaquo;</button>
2323
+ <button type="button" class="btn btn-outline-secondary btn-sm" id="onekite-wiz-prev" aria-label="Mois précédent">&lsaquo;</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">&rsaquo;</button>
2234
2326
  </div>
2235
2327
  <div class="onekite-range-weekdays">
2236
2328
  <div>L</div><div>M</div><div>M</div><div>J</div><div>V</div><div>S</div><div>D</div>
2237
2329
  </div>
2238
- <div class="onekite-range-grid" id="onekite-range-grid"></div>
2239
- <div class="onekite-range-summary" id="onekite-range-summary"></div>
2330
+ <div class="onekite-range-grid" id="onekite-wiz-grid"></div>
2331
+ <div class="onekite-range-summary" id="onekite-wiz-summary"></div>
2240
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>
2241
2353
  </div>
2242
2354
  `;
2243
2355
 
2244
2356
  const dlg = bootbox.dialog({
2245
- title: 'Choisir une période',
2357
+ title: 'Créer',
2246
2358
  message: html,
2247
2359
  buttons: {
2248
- cancel: { label: 'Annuler', className: 'btn-secondary' },
2249
- ok: {
2250
- label: 'Continuer',
2251
- className: 'btn-primary',
2252
- callback: function () {
2253
- const s = state.start;
2254
- const e = state.end || state.start;
2255
- if (!s) {
2256
- alerts.error('Choisis une date de début.');
2257
- return false;
2258
- }
2259
- if (!e) {
2260
- alerts.error('Choisis une date de fin.');
2261
- return false;
2262
- }
2263
- // Convert end inclusive -> end exclusive (FullCalendar rule)
2264
- const endExcl = new Date(e);
2265
- endExcl.setDate(endExcl.getDate() + 1);
2266
-
2267
- // If a callback is provided, delegate the next step (chooser).
2268
- if (typeof onSelection === 'function') {
2269
- try { onSelection({ start: s, end: endExcl, allDay: true }); } catch (e) {}
2270
- return true;
2271
- }
2272
-
2273
- (async () => {
2274
- try {
2275
- if (isDialogOpen) return;
2276
- isDialogOpen = true;
2277
- const items = cachedItems || (await loadItems());
2278
- const chosen = await openReservationDialog({ start: s, end: endExcl }, items);
2279
- if (chosen && chosen.itemIds && chosen.itemIds.length) {
2280
- const startDate = toLocalYmd(s);
2281
- const endDate = (chosen && chosen.endDate) ? String(chosen.endDate) : toLocalYmd(endExcl);
2282
- const resp = await requestReservation({
2283
- start: startDate,
2284
- end: endDate,
2285
- itemIds: chosen.itemIds,
2286
- itemNames: chosen.itemNames,
2287
- total: chosen.total,
2288
- });
2289
- if (resp && (resp.autoPaid || String(resp.status) === 'paid')) {
2290
- showAlert('success', 'Réservation confirmée.');
2291
- } else {
2292
- showAlert('success', 'Demande envoyée (en attente de validation).');
2293
- }
2294
- invalidateEventsCache();
2295
- if (currentCalendar) scheduleRefetch(currentCalendar);
2296
- }
2297
- } catch (err) {
2298
- // ignore
2299
- }
2300
- })();
2301
-
2302
- return true;
2303
- },
2304
- },
2360
+ cancel: { label: 'Annuler', className: 'btn-danger' },
2305
2361
  },
2306
2362
  });
2307
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
+
2308
2370
  function sameDay(a, b) {
2309
2371
  return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2310
2372
  }
@@ -2324,9 +2386,10 @@ async function openFabDatePicker(onSelection) {
2324
2386
  }
2325
2387
 
2326
2388
  function render() {
2327
- const monthEl = document.getElementById('onekite-range-month');
2328
- const gridEl = document.getElementById('onekite-range-grid');
2329
- const sumEl = document.getElementById('onekite-range-summary');
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');
2330
2393
  if (!monthEl || !gridEl || !sumEl) return;
2331
2394
 
2332
2395
  monthEl.textContent = monthLabel(state.cursor);
@@ -2386,6 +2449,12 @@ async function openFabDatePicker(onSelection) {
2386
2449
  state.end = null;
2387
2450
  // If cursor month is before selected month, keep cursor
2388
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
+ }
2389
2458
  return;
2390
2459
  }
2391
2460
  // Second click: set end (inclusive)
@@ -2397,6 +2466,10 @@ async function openFabDatePicker(onSelection) {
2397
2466
  }
2398
2467
  state.end = clicked;
2399
2468
  render();
2469
+ if (!didAutoScrollToType) {
2470
+ didAutoScrollToType = true;
2471
+ try { document.getElementById('onekite-wiz-type')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
2472
+ }
2400
2473
  });
2401
2474
  }
2402
2475
 
@@ -2417,48 +2490,100 @@ async function openFabDatePicker(onSelection) {
2417
2490
 
2418
2491
  dlg.on('shown.bs.modal', () => {
2419
2492
  try {
2420
- const prevBtn = document.getElementById('onekite-range-prev');
2421
- const nextBtn = document.getElementById('onekite-range-next');
2422
- if (prevBtn) {
2423
- prevBtn.addEventListener('click', () => {
2424
- state.cursor.setMonth(state.cursor.getMonth() - 1);
2425
- render();
2426
- });
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) {}
2427
2541
  }
2428
- if (nextBtn) {
2429
- nextBtn.addEventListener('click', () => {
2430
- state.cursor.setMonth(state.cursor.getMonth() + 1);
2431
- render();
2432
- });
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 };
2433
2554
  }
2434
2555
 
2435
- // Swipe left/right to change month
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
+ }
2570
+ }
2571
+
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.
2436
2580
  try {
2437
- const root = document.getElementById('onekite-range-picker');
2438
- if (root) {
2439
- let x0 = null;
2440
- root.addEventListener('touchstart', (ev) => {
2441
- try { x0 = ev.touches && ev.touches[0] ? ev.touches[0].clientX : null; } catch (e) { x0 = null; }
2442
- }, { passive: true });
2443
- root.addEventListener('touchend', (ev) => {
2444
- try {
2445
- if (x0 === null) return;
2446
- const x1 = ev.changedTouches && ev.changedTouches[0] ? ev.changedTouches[0].clientX : null;
2447
- if (x1 === null) return;
2448
- const dx = x1 - x0;
2449
- if (Math.abs(dx) < 40) return;
2450
- if (dx < 0) {
2451
- state.cursor.setMonth(state.cursor.getMonth() + 1);
2452
- } else {
2453
- state.cursor.setMonth(state.cursor.getMonth() - 1);
2454
- }
2455
- render();
2456
- } catch (e) {}
2457
- finally { x0 = null; }
2458
- }, { passive: true });
2581
+ if (state.start) {
2582
+ setTimeout(() => {
2583
+ try { document.getElementById('onekite-wiz-type')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
2584
+ }, 150);
2459
2585
  }
2460
2586
  } catch (e) {}
2461
- render();
2462
2587
  } catch (e) {}
2463
2588
  });
2464
2589
  }
@@ -2490,10 +2615,10 @@ function parseYmdDate(ymdStr) {
2490
2615
  fabEl.innerHTML = '<i class="fa fa-plus"></i>';
2491
2616
 
2492
2617
  fabHandler = () => {
2493
- // init() sets createFromSelectionHandler. If the calendar has not
2618
+ // init() sets openCreateWizardHandler. If the calendar has not
2494
2619
  // finished initialising, do nothing.
2495
- if (typeof createFromSelectionHandler !== 'function') return;
2496
- openFabDatePicker(createFromSelectionHandler);
2620
+ if (typeof openCreateWizardHandler !== 'function') return;
2621
+ openCreateWizardHandler();
2497
2622
  };
2498
2623
  fabEl.addEventListener('click', fabHandler);
2499
2624