nodebb-plugin-onekite-calendar 2.0.57 → 2.0.59

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 CHANGED
@@ -996,6 +996,29 @@ api.joinSpecialEvent = async function (req, res) {
996
996
  });
997
997
  };
998
998
 
999
+ api.leaveSpecialEvent = async function (req, res) {
1000
+ const settings = await getSettings();
1001
+ const uid = req.uid;
1002
+ if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1003
+ const ok = await canCreateSpecial(uid, settings);
1004
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
1005
+
1006
+ const eid = String(req.params.eid || '').replace(/^special:/, '').trim();
1007
+ if (!eid) return res.status(400).json({ error: 'bad-id' });
1008
+
1009
+ const ev = await dbLayer.getSpecialEvent(eid);
1010
+ if (!ev) return res.status(404).json({ error: 'not-found' });
1011
+
1012
+ const list = normalizeUidList(ev.participants);
1013
+ const sUid = String(uid);
1014
+ const next = list.filter((x) => String(x) !== sUid);
1015
+ ev.participants = JSON.stringify(next);
1016
+ await dbLayer.saveSpecialEvent(ev);
1017
+
1018
+ realtime.emitCalendarUpdated({ kind: 'specialEvent', action: 'participants', eid });
1019
+ return res.json({ ok: true, participants: next, participantsUsernames: await usernamesByUids(next) });
1020
+ };
1021
+
999
1022
  api.getCapabilities = async function (req, res) {
1000
1023
  const settings = await getSettings();
1001
1024
  const uid = req.uid || 0;
@@ -1163,6 +1186,30 @@ api.joinOuting = async function (req, res) {
1163
1186
  return res.json({ ok: true, participants: list, participantsUsernames: await usernamesByUids(list) });
1164
1187
  };
1165
1188
 
1189
+ api.leaveOuting = async function (req, res) {
1190
+ const settings = await getSettings();
1191
+ const uid = req.uid;
1192
+ if (!uid) return res.status(401).json({ error: 'not-logged-in' });
1193
+ // Outings share the same rights as reservations/locations.
1194
+ const ok = await canRequest(uid, settings, Date.now());
1195
+ if (!ok) return res.status(403).json({ error: 'not-allowed' });
1196
+
1197
+ const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
1198
+ if (!oid) return res.status(400).json({ error: 'bad-id' });
1199
+
1200
+ const o = await dbLayer.getOuting(oid);
1201
+ if (!o) return res.status(404).json({ error: 'not-found' });
1202
+
1203
+ const list = normalizeUidList(o.participants);
1204
+ const sUid = String(uid);
1205
+ const next = list.filter((x) => String(x) !== sUid);
1206
+ o.participants = JSON.stringify(next);
1207
+ await dbLayer.saveOuting(o);
1208
+
1209
+ realtime.emitCalendarUpdated({ kind: 'outing', action: 'participants', oid });
1210
+ return res.json({ ok: true, participants: next, participantsUsernames: await usernamesByUids(next) });
1211
+ };
1212
+
1166
1213
  api.createOuting = async function (req, res) {
1167
1214
  const settings = await getSettings();
1168
1215
  if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
package/library.js CHANGED
@@ -84,6 +84,7 @@ Plugin.init = async function (params) {
84
84
  router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
85
85
  // Participants (self-join) for special events
86
86
  router.post('/api/v3/plugins/calendar-onekite/special-events/:eid/participants', ...publicExpose, api.joinSpecialEvent);
87
+ router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid/participants', ...publicExpose, api.leaveSpecialEvent);
87
88
 
88
89
  // Outings (prévisions de sortie)
89
90
  router.post('/api/v3/plugins/calendar-onekite/outings', ...publicExpose, api.createOuting);
@@ -91,6 +92,7 @@ Plugin.init = async function (params) {
91
92
  router.delete('/api/v3/plugins/calendar-onekite/outings/:oid', ...publicExpose, api.deleteOuting);
92
93
  // Participants (self-join) for outings
93
94
  router.post('/api/v3/plugins/calendar-onekite/outings/:oid/participants', ...publicExpose, api.joinOuting);
95
+ router.delete('/api/v3/plugins/calendar-onekite/outings/:oid/participants', ...publicExpose, api.leaveOuting);
94
96
 
95
97
  // Calendar export (ICS)
96
98
  // Note: reservations are protected by a signature in the querystring.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "2.0.57",
3
+ "version": "2.0.59",
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.57"
42
+ "version": "2.0.59"
43
43
  }
package/public/client.js CHANGED
@@ -1675,10 +1675,15 @@ function toDatetimeLocalValue(date) {
1675
1675
  const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
1676
1676
  const notes = String(p.notes || '').trim();
1677
1677
  const participants = Array.isArray(p.participantsUsernames) ? p.participantsUsernames : [];
1678
+ const eidPlain = escapeHtml(String(p.eid || '').replace(/^special:/, ''));
1679
+ // Use real buttons with visible + / - text to avoid relying on icon fonts.
1678
1680
  const joinBtn = (p.canJoin && !p.isParticipant)
1679
- ? `<a href="#" class="onekite-join-special ms-2" data-eid="${escapeHtml(String(p.eid || '').replace(/^special:/, ''))}" title="S'ajouter"><i class="fa fa-plus"></i></a>`
1681
+ ? `<button type="button" class="btn btn-sm btn-success ms-2 onekite-join-special" data-eid="${eidPlain}" title="S'ajouter" aria-label="S'ajouter">+</button>`
1680
1682
  : '';
1681
- const participantsHtml = `<div class="mb-2" id="onekite-participants-special"><strong>Participants</strong>${joinBtn}<br>` +
1683
+ const leaveBtn = (p.canJoin && p.isParticipant)
1684
+ ? `<button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-special" data-eid="${eidPlain}" title="Se retirer" aria-label="Se retirer">−</button>`
1685
+ : '';
1686
+ const participantsHtml = `<div class="mb-2" id="onekite-participants-special"><strong>Participants</strong>${joinBtn}${leaveBtn}<br>` +
1682
1687
  (participants.length
1683
1688
  ? participants.map((name) => {
1684
1689
  const u = String(name || '').trim();
@@ -1755,12 +1760,38 @@ function toDatetimeLocalValue(date) {
1755
1760
  return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
1756
1761
  }).filter(Boolean).join('')
1757
1762
  : `<span class="text-muted">Aucun</span>`;
1758
- box.html(`<strong>Participants</strong><br>${links}`);
1763
+ box.html(`<strong>Participants</strong><button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-special" data-eid="${escapeHtml(eid)}" title="Se retirer" aria-label="Se retirer">−</button><br>${links}`);
1759
1764
  }
1760
1765
  } catch (e3) {
1761
1766
  showAlert('error', "Impossible de s'ajouter.");
1762
1767
  }
1763
1768
  });
1769
+
1770
+ dlg.find('.onekite-leave-special').off('click').on('click', async (e2) => {
1771
+ e2.preventDefault();
1772
+ const btn = e2.currentTarget;
1773
+ if (!btn) return;
1774
+ try {
1775
+ btn.classList.add('disabled');
1776
+ const eid = String(btn.getAttribute('data-eid') || '').trim();
1777
+ if (!eid) return;
1778
+ const r = await fetchJson(`/api/v3/plugins/calendar-onekite/special-events/${encodeURIComponent(eid)}/participants`, { method: 'DELETE' });
1779
+ const names = Array.isArray(r && r.participantsUsernames) ? r.participantsUsernames : [];
1780
+ const box = dlg.find('#onekite-participants-special');
1781
+ if (box && box.length) {
1782
+ const links = names.length
1783
+ ? names.map((name) => {
1784
+ const u = String(name || '').trim();
1785
+ if (!u) return '';
1786
+ return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
1787
+ }).filter(Boolean).join('')
1788
+ : `<span class="text-muted">Aucun</span>`;
1789
+ box.html(`<strong>Participants</strong><button type="button" class="btn btn-sm btn-success ms-2 onekite-join-special" data-eid="${escapeHtml(eid)}" title="S'ajouter" aria-label="S'ajouter">+</button><br>${links}`);
1790
+ }
1791
+ } catch (e3) {
1792
+ showAlert('error', "Impossible de se retirer.");
1793
+ }
1794
+ });
1764
1795
  });
1765
1796
  } catch (e) {}
1766
1797
  try {
@@ -1783,10 +1814,15 @@ function toDatetimeLocalValue(date) {
1783
1814
  const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
1784
1815
  const notes = String(p.notes || '').trim();
1785
1816
  const participants = Array.isArray(p.participantsUsernames) ? p.participantsUsernames : [];
1817
+ const oidPlain = escapeHtml(String(p.oid || '').replace(/^outing:/, ''));
1818
+ // Use real buttons with visible + / - text to avoid relying on icon fonts.
1786
1819
  const joinBtn = (p.canJoin && !p.isParticipant)
1787
- ? `<a href="#" class="onekite-join-outing ms-2" data-oid="${escapeHtml(String(p.oid || '').replace(/^outing:/, ''))}" title="S'ajouter"><i class="fa fa-plus"></i></a>`
1820
+ ? `<button type="button" class="btn btn-sm btn-success ms-2 onekite-join-outing" data-oid="${oidPlain}" title="S'ajouter" aria-label="S'ajouter">+</button>`
1788
1821
  : '';
1789
- const participantsHtml = `<div class="mb-2" id="onekite-participants-outing"><strong>Participants</strong>${joinBtn}<br>` +
1822
+ const leaveBtn = (p.canJoin && p.isParticipant)
1823
+ ? `<button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-outing" data-oid="${oidPlain}" title="Se retirer" aria-label="Se retirer">−</button>`
1824
+ : '';
1825
+ const participantsHtml = `<div class="mb-2" id="onekite-participants-outing"><strong>Participants</strong>${joinBtn}${leaveBtn}<br>` +
1790
1826
  (participants.length
1791
1827
  ? participants.map((name) => {
1792
1828
  const u = String(name || '').trim();
@@ -1863,12 +1899,38 @@ function toDatetimeLocalValue(date) {
1863
1899
  return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
1864
1900
  }).filter(Boolean).join('')
1865
1901
  : `<span class="text-muted">Aucun</span>`;
1866
- box.html(`<strong>Participants</strong><br>${links}`);
1902
+ box.html(`<strong>Participants</strong><button type="button" class="btn btn-sm btn-danger ms-2 onekite-leave-outing" data-oid="${escapeHtml(oid)}" title="Se retirer" aria-label="Se retirer">−</button><br>${links}`);
1867
1903
  }
1868
1904
  } catch (e3) {
1869
1905
  showAlert('error', "Impossible de s'ajouter.");
1870
1906
  }
1871
1907
  });
1908
+
1909
+ dlg.find('.onekite-leave-outing').off('click').on('click', async (e2) => {
1910
+ e2.preventDefault();
1911
+ const btn = e2.currentTarget;
1912
+ if (!btn) return;
1913
+ try {
1914
+ btn.classList.add('disabled');
1915
+ const oid = String(btn.getAttribute('data-oid') || '').trim();
1916
+ if (!oid) return;
1917
+ const r = await fetchJson(`/api/v3/plugins/calendar-onekite/outings/${encodeURIComponent(oid)}/participants`, { method: 'DELETE' });
1918
+ const names = Array.isArray(r && r.participantsUsernames) ? r.participantsUsernames : [];
1919
+ const box = dlg.find('#onekite-participants-outing');
1920
+ if (box && box.length) {
1921
+ const links = names.length
1922
+ ? names.map((name) => {
1923
+ const u = String(name || '').trim();
1924
+ if (!u) return '';
1925
+ return `<a class="onekite-user-link me-2" href="${window.location.origin}/user/${encodeURIComponent(u)}">${escapeHtml(u)}</a>`;
1926
+ }).filter(Boolean).join('')
1927
+ : `<span class="text-muted">Aucun</span>`;
1928
+ box.html(`<strong>Participants</strong><button type="button" class="btn btn-sm btn-success ms-2 onekite-join-outing" data-oid="${escapeHtml(oid)}" title="S'ajouter" aria-label="S'ajouter">+</button><br>${links}`);
1929
+ }
1930
+ } catch (e3) {
1931
+ showAlert('error', "Impossible de se retirer.");
1932
+ }
1933
+ });
1872
1934
  });
1873
1935
  } catch (e) {}
1874
1936
  try {