nodebb-plugin-onekite-calendar 2.0.52 → 2.0.53
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 +6 -4
- package/lib/api.js +18 -18
- package/lib/scheduler.js +3 -2
- package/lib/settings.js +36 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +15 -12
- package/templates/calendar-onekite.tpl +31 -0
package/lib/admin.js
CHANGED
|
@@ -4,6 +4,7 @@ const meta = require.main.require('./src/meta');
|
|
|
4
4
|
const user = require.main.require('./src/user');
|
|
5
5
|
const emailer = require.main.require('./src/emailer');
|
|
6
6
|
const { forumBaseUrl, formatFR } = require('./utils');
|
|
7
|
+
const { getSettings, invalidateSettings } = require('./settings');
|
|
7
8
|
const realtime = require('./realtime');
|
|
8
9
|
const crypto = require('crypto');
|
|
9
10
|
const nconf = require.main.require('nconf');
|
|
@@ -108,12 +109,13 @@ admin.renderAdmin = async function (req, res) {
|
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
admin.getSettings = async function (req, res) {
|
|
111
|
-
const settings = await
|
|
112
|
+
const settings = await getSettings();
|
|
112
113
|
res.json(settings || {});
|
|
113
114
|
};
|
|
114
115
|
|
|
115
116
|
admin.saveSettings = async function (req, res) {
|
|
116
117
|
await meta.settings.set('calendar-onekite', req.body || {});
|
|
118
|
+
invalidateSettings();
|
|
117
119
|
res.json({ ok: true });
|
|
118
120
|
};
|
|
119
121
|
|
|
@@ -173,7 +175,7 @@ admin.approveReservation = async function (req, res) {
|
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
// Create HelloAsso payment link if configured
|
|
176
|
-
const settings = await
|
|
178
|
+
const settings = await getSettings();
|
|
177
179
|
const env = settings.helloassoEnv || 'prod';
|
|
178
180
|
const token = await helloasso.getAccessToken({
|
|
179
181
|
env,
|
|
@@ -378,7 +380,7 @@ admin.purgeOutingsByYear = async function (req, res) {
|
|
|
378
380
|
|
|
379
381
|
|
|
380
382
|
admin.debugHelloAsso = async function (req, res) {
|
|
381
|
-
const settings = await
|
|
383
|
+
const settings = await getSettings();
|
|
382
384
|
const env = (settings && settings.helloassoEnv) || 'prod';
|
|
383
385
|
|
|
384
386
|
// Never expose secrets in debug output
|
|
@@ -531,7 +533,7 @@ admin.getAccounting = async function (req, res) {
|
|
|
531
533
|
if (!Number.isFinite(y)) return null;
|
|
532
534
|
if (catalogByYear.has(y)) return catalogByYear.get(y);
|
|
533
535
|
try {
|
|
534
|
-
if (!settingsCache) settingsCache = await
|
|
536
|
+
if (!settingsCache) settingsCache = await getSettings();
|
|
535
537
|
if (!settingsCache || !settingsCache.helloassoClientId || !settingsCache.helloassoClientSecret || !settingsCache.helloassoOrganizationSlug || !settingsCache.helloassoFormType) {
|
|
536
538
|
catalogByYear.set(y, null);
|
|
537
539
|
return null;
|
package/lib/api.js
CHANGED
|
@@ -11,7 +11,7 @@ const db = require.main.require('./src/database');
|
|
|
11
11
|
const logger = require.main.require('./src/logger');
|
|
12
12
|
|
|
13
13
|
const dbLayer = require('./db');
|
|
14
|
-
const {
|
|
14
|
+
const { getSettings } = require('./settings');
|
|
15
15
|
|
|
16
16
|
// Resolve group identifiers from ACP.
|
|
17
17
|
// Admins may enter a group *name* ("Test") or a *slug* ("onekite-ffvl-2026").
|
|
@@ -331,7 +331,7 @@ async function canValidate(uid, settings) {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
async function canModerate(uid) {
|
|
334
|
-
const settings = await
|
|
334
|
+
const settings = await getSettings();
|
|
335
335
|
return await canValidate(uid, settings);
|
|
336
336
|
}
|
|
337
337
|
|
|
@@ -634,7 +634,7 @@ api.getEvents = async function (req, res) {
|
|
|
634
634
|
const startTs = toTs(qStartRaw) || 0;
|
|
635
635
|
const endTs = toTs(qEndRaw) || (Date.now() + 365 * 24 * 3600 * 1000);
|
|
636
636
|
|
|
637
|
-
const settings = await
|
|
637
|
+
const settings = await getSettings();
|
|
638
638
|
const canMod = req.uid ? await canValidate(req.uid, settings) : false;
|
|
639
639
|
const widgetMode = String((req.query && req.query.widget) || '') === '1';
|
|
640
640
|
const canSpecialCreate = req.uid ? await canCreateSpecial(req.uid, settings) : false;
|
|
@@ -826,7 +826,7 @@ api.getReservationDetails = async function (req, res) {
|
|
|
826
826
|
const uid = req.uid;
|
|
827
827
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
828
828
|
|
|
829
|
-
const settings = await
|
|
829
|
+
const settings = await getSettings();
|
|
830
830
|
const canMod = await canValidate(uid, settings);
|
|
831
831
|
|
|
832
832
|
const rid = String(req.params.rid || '').trim();
|
|
@@ -873,7 +873,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
873
873
|
const uid = req.uid;
|
|
874
874
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
875
875
|
|
|
876
|
-
const settings = await
|
|
876
|
+
const settings = await getSettings();
|
|
877
877
|
const canMod = await canValidate(uid, settings);
|
|
878
878
|
const canSpecialDelete = await canDeleteSpecial(uid, settings);
|
|
879
879
|
|
|
@@ -917,7 +917,7 @@ api.getSpecialEventDetails = async function (req, res) {
|
|
|
917
917
|
};
|
|
918
918
|
|
|
919
919
|
api.getCapabilities = async function (req, res) {
|
|
920
|
-
const settings = await
|
|
920
|
+
const settings = await getSettings();
|
|
921
921
|
const uid = req.uid || 0;
|
|
922
922
|
const canMod = uid ? await canValidate(uid, settings) : false;
|
|
923
923
|
res.json({
|
|
@@ -931,7 +931,7 @@ api.getCapabilities = async function (req, res) {
|
|
|
931
931
|
};
|
|
932
932
|
|
|
933
933
|
api.createSpecialEvent = async function (req, res) {
|
|
934
|
-
const settings = await
|
|
934
|
+
const settings = await getSettings();
|
|
935
935
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
936
936
|
const ok = await canCreateSpecial(req.uid, settings);
|
|
937
937
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -987,7 +987,7 @@ api.createSpecialEvent = async function (req, res) {
|
|
|
987
987
|
};
|
|
988
988
|
|
|
989
989
|
api.deleteSpecialEvent = async function (req, res) {
|
|
990
|
-
const settings = await
|
|
990
|
+
const settings = await getSettings();
|
|
991
991
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
992
992
|
const ok = await canDeleteSpecial(req.uid, settings);
|
|
993
993
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
@@ -1007,7 +1007,7 @@ api.getOutingDetails = async function (req, res) {
|
|
|
1007
1007
|
const uid = req.uid;
|
|
1008
1008
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1009
1009
|
|
|
1010
|
-
const settings = await
|
|
1010
|
+
const settings = await getSettings();
|
|
1011
1011
|
const canMod = await canValidate(uid, settings);
|
|
1012
1012
|
|
|
1013
1013
|
const oid = String(req.params.oid || '').trim();
|
|
@@ -1048,7 +1048,7 @@ api.getOutingDetails = async function (req, res) {
|
|
|
1048
1048
|
};
|
|
1049
1049
|
|
|
1050
1050
|
api.createOuting = async function (req, res) {
|
|
1051
|
-
const settings = await
|
|
1051
|
+
const settings = await getSettings();
|
|
1052
1052
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1053
1053
|
|
|
1054
1054
|
const startTs = toTs(req.body && req.body.start);
|
|
@@ -1115,7 +1115,7 @@ api.createOuting = async function (req, res) {
|
|
|
1115
1115
|
};
|
|
1116
1116
|
|
|
1117
1117
|
api.deleteOuting = async function (req, res) {
|
|
1118
|
-
const settings = await
|
|
1118
|
+
const settings = await getSettings();
|
|
1119
1119
|
if (!req.uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1120
1120
|
|
|
1121
1121
|
const oid = String(req.params.oid || '').replace(/^outing:/, '').trim();
|
|
@@ -1139,7 +1139,7 @@ api.deleteOuting = async function (req, res) {
|
|
|
1139
1139
|
};
|
|
1140
1140
|
|
|
1141
1141
|
api.getItems = async function (req, res) {
|
|
1142
|
-
const settings = await
|
|
1142
|
+
const settings = await getSettings();
|
|
1143
1143
|
|
|
1144
1144
|
const env = settings.helloassoEnv || 'prod';
|
|
1145
1145
|
const token = await helloasso.getAccessToken({
|
|
@@ -1191,7 +1191,7 @@ api.createReservation = async function (req, res) {
|
|
|
1191
1191
|
const uid = req.uid;
|
|
1192
1192
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1193
1193
|
|
|
1194
|
-
const settings = await
|
|
1194
|
+
const settings = await getSettings();
|
|
1195
1195
|
const startPreview = toTs(req.body.start);
|
|
1196
1196
|
const ok = await canRequest(uid, settings, startPreview);
|
|
1197
1197
|
if (!ok) {
|
|
@@ -1438,7 +1438,7 @@ api.createReservation = async function (req, res) {
|
|
|
1438
1438
|
api.approveReservation = async function (req, res) {
|
|
1439
1439
|
const uid = req.uid;
|
|
1440
1440
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1441
|
-
const settings = await
|
|
1441
|
+
const settings = await getSettings();
|
|
1442
1442
|
const ok = await canValidate(uid, settings);
|
|
1443
1443
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
1444
1444
|
|
|
@@ -1471,7 +1471,7 @@ api.approveReservation = async function (req, res) {
|
|
|
1471
1471
|
}
|
|
1472
1472
|
// Create HelloAsso payment link on validation
|
|
1473
1473
|
try {
|
|
1474
|
-
const settings2 = await
|
|
1474
|
+
const settings2 = await getSettings();
|
|
1475
1475
|
const token = await helloasso.getAccessToken({ env: settings2.helloassoEnv || 'prod', clientId: settings2.helloassoClientId, clientSecret: settings2.helloassoClientSecret });
|
|
1476
1476
|
const payer = await user.getUserFields(r.uid, ['email']);
|
|
1477
1477
|
const year = yearFromTs(r.start);
|
|
@@ -1619,7 +1619,7 @@ api.approveReservation = async function (req, res) {
|
|
|
1619
1619
|
api.refuseReservation = async function (req, res) {
|
|
1620
1620
|
const uid = req.uid;
|
|
1621
1621
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1622
|
-
const settings = await
|
|
1622
|
+
const settings = await getSettings();
|
|
1623
1623
|
const ok = await canValidate(uid, settings);
|
|
1624
1624
|
if (!ok) return res.status(403).json({ error: 'not-allowed' });
|
|
1625
1625
|
|
|
@@ -1679,7 +1679,7 @@ api.cancelReservation = async function (req, res) {
|
|
|
1679
1679
|
const uid = req.uid;
|
|
1680
1680
|
if (!uid) return res.status(401).json({ error: 'not-logged-in' });
|
|
1681
1681
|
|
|
1682
|
-
const settings = await
|
|
1682
|
+
const settings = await getSettings();
|
|
1683
1683
|
const rid = String(req.params.rid || '').trim();
|
|
1684
1684
|
if (!rid) return res.status(400).json({ error: 'missing-rid' });
|
|
1685
1685
|
|
|
@@ -1806,7 +1806,7 @@ api.setMaintenanceAll = async function (req, res) {
|
|
|
1806
1806
|
// When enabling, we need the current catalog IDs (HelloAsso shop)
|
|
1807
1807
|
let catalogIds = [];
|
|
1808
1808
|
if (enabled) {
|
|
1809
|
-
const settings = await
|
|
1809
|
+
const settings = await getSettings();
|
|
1810
1810
|
const env = settings.helloassoEnv || 'prod';
|
|
1811
1811
|
const token = await helloasso.getAccessToken({
|
|
1812
1812
|
env,
|
package/lib/scheduler.js
CHANGED
|
@@ -8,6 +8,7 @@ const realtime = require('./realtime');
|
|
|
8
8
|
const nconf = require.main.require('nconf');
|
|
9
9
|
const groups = require.main.require('./src/groups');
|
|
10
10
|
const utils = require('./utils');
|
|
11
|
+
const { getSettings } = require('./settings');
|
|
11
12
|
|
|
12
13
|
let timer = null;
|
|
13
14
|
|
|
@@ -138,7 +139,7 @@ async function getNotifiedValidatorUids(settings) {
|
|
|
138
139
|
|
|
139
140
|
// Pending holds: short lock after a user creates a request (defaults to 5 minutes)
|
|
140
141
|
async function expirePending() {
|
|
141
|
-
const settings = await
|
|
142
|
+
const settings = await getSettings();
|
|
142
143
|
const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
143
144
|
const validatorReminderMins = parseInt(getSetting(settings, 'validatorReminderMinutesPending', '0'), 10) || 0;
|
|
144
145
|
const now = Date.now();
|
|
@@ -257,7 +258,7 @@ async function expirePending() {
|
|
|
257
258
|
// - We send a reminder after `paymentHoldMinutes` (default 60)
|
|
258
259
|
// - We expire (and remove) after `2 * paymentHoldMinutes`
|
|
259
260
|
async function processAwaitingPayment() {
|
|
260
|
-
const settings = await
|
|
261
|
+
const settings = await getSettings();
|
|
261
262
|
const holdMins = parseInt(
|
|
262
263
|
getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
|
|
263
264
|
10
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const meta = require.main.require('./src/meta');
|
|
4
|
+
|
|
5
|
+
const SETTINGS_KEY = 'calendar-onekite';
|
|
6
|
+
|
|
7
|
+
// Simple in-memory cache to reduce repeated meta.settings.get calls.
|
|
8
|
+
// Per-process only (NodeBB is often multi-process), but still beneficial.
|
|
9
|
+
const CACHE_TTL_MS = 5000;
|
|
10
|
+
let cachedSettings = null;
|
|
11
|
+
let cachedAtMs = 0;
|
|
12
|
+
|
|
13
|
+
async function getSettings(options) {
|
|
14
|
+
const opts = options || {};
|
|
15
|
+
const force = !!opts.force;
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
|
|
18
|
+
if (!force && cachedSettings && (now - cachedAtMs) < CACHE_TTL_MS) {
|
|
19
|
+
return cachedSettings;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
cachedSettings = await meta.settings.get(SETTINGS_KEY);
|
|
23
|
+
cachedAtMs = now;
|
|
24
|
+
return cachedSettings || {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function invalidateSettings() {
|
|
28
|
+
cachedSettings = null;
|
|
29
|
+
cachedAtMs = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
SETTINGS_KEY,
|
|
34
|
+
getSettings,
|
|
35
|
+
invalidateSettings,
|
|
36
|
+
};
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/client.js
CHANGED
|
@@ -1045,10 +1045,6 @@ function toDatetimeLocalValue(date) {
|
|
|
1045
1045
|
const endDisplay = endInclusiveForDisplay(start, end);
|
|
1046
1046
|
|
|
1047
1047
|
const messageHtml = `
|
|
1048
|
-
<div class="mb-2">
|
|
1049
|
-
<label class="form-label">Titre (facultatif)</label>
|
|
1050
|
-
<input type="text" class="form-control" id="onekite-res-title" placeholder="Ex: ..." />
|
|
1051
|
-
</div>
|
|
1052
1048
|
<div class="mb-2" id="onekite-period"><strong>Période</strong><br>${formatDt(start)} → ${formatDt(endDisplay)} <span class="text-muted" id="onekite-days">(${days} jour${days > 1 ? 's' : ''})</span></div>
|
|
1053
1049
|
${shortcutsHtml}
|
|
1054
1050
|
<div class="mb-2"><strong>Matériel</strong></div>
|
|
@@ -1084,7 +1080,6 @@ function toDatetimeLocalValue(date) {
|
|
|
1084
1080
|
label: 'Envoyer',
|
|
1085
1081
|
className: 'btn-primary',
|
|
1086
1082
|
callback: function () {
|
|
1087
|
-
const title = (document.getElementById('onekite-res-title')?.value || '').trim();
|
|
1088
1083
|
const cbs = Array.from(document.querySelectorAll('.onekite-item-cb')).filter(cb => cb.checked);
|
|
1089
1084
|
if (!cbs.length) {
|
|
1090
1085
|
showAlert('error', 'Choisis au moins un matériel.');
|
|
@@ -1096,7 +1091,7 @@ function toDatetimeLocalValue(date) {
|
|
|
1096
1091
|
const total = (sum / 100) * days;
|
|
1097
1092
|
// Return the effective end date (exclusive) because duration shortcuts can
|
|
1098
1093
|
// change the range without updating the original FullCalendar selection.
|
|
1099
|
-
resolve({
|
|
1094
|
+
resolve({ itemIds, itemNames, total, days, endDate: toLocalYmd(end) });
|
|
1100
1095
|
},
|
|
1101
1096
|
},
|
|
1102
1097
|
},
|
|
@@ -1561,8 +1556,8 @@ function toDatetimeLocalValue(date) {
|
|
|
1561
1556
|
|
|
1562
1557
|
// IMPORTANT: align "special" events and "outings" display exactly like
|
|
1563
1558
|
// reservation icons.
|
|
1564
|
-
// We inject the
|
|
1565
|
-
//
|
|
1559
|
+
// We inject the time range directly into the event title so FC doesn't
|
|
1560
|
+
// reserve a separate time column (which creates a leading gap).
|
|
1566
1561
|
const mapped = (Array.isArray(data) ? data : []).map((ev) => {
|
|
1567
1562
|
try {
|
|
1568
1563
|
if (ev && ev.extendedProps && (ev.extendedProps.type === 'special' || ev.extendedProps.type === 'outing') && ev.start && ev.end) {
|
|
@@ -1575,10 +1570,18 @@ function toDatetimeLocalValue(date) {
|
|
|
1575
1570
|
const ts = `${pad2(s.getHours())}:${pad2(s.getMinutes())}`;
|
|
1576
1571
|
const te = `${pad2(e.getHours())}:${pad2(e.getMinutes())}`;
|
|
1577
1572
|
const baseTitle = (ev.title || '').trim();
|
|
1578
|
-
// Avoid double prefix on rerenders
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1573
|
+
// Avoid double prefix on rerenders.
|
|
1574
|
+
// UX request: no clock icon for "prévision de sortie" (outing).
|
|
1575
|
+
const isOuting = ev.extendedProps.type === 'outing';
|
|
1576
|
+
const prefix = isOuting ? `${ts}-${te} ` : `🕒 ${ts}-${te} `;
|
|
1577
|
+
if (isOuting) {
|
|
1578
|
+
if (!/^\d{2}:\d{2}-\d{2}:\d{2}\s/.test(baseTitle)) {
|
|
1579
|
+
ev.title = `${prefix}${baseTitle}`.trim();
|
|
1580
|
+
}
|
|
1581
|
+
} else {
|
|
1582
|
+
if (!baseTitle.startsWith('🕒 ')) {
|
|
1583
|
+
ev.title = `${prefix}${baseTitle}`.trim();
|
|
1584
|
+
}
|
|
1582
1585
|
}
|
|
1583
1586
|
}
|
|
1584
1587
|
} catch (e2) {}
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
<div class="row">
|
|
3
3
|
<div class="col-12">
|
|
4
4
|
<h1>Calendrier</h1>
|
|
5
|
+
<div class="onekite-calendar-legend" aria-label="Légende des couleurs" style="margin:.25rem 0 .75rem;">
|
|
6
|
+
<span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#f39c12"></span>En attente</span>
|
|
7
|
+
<span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#d35400"></span>Paiement en attente</span>
|
|
8
|
+
<span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#27ae60"></span>Payée</span>
|
|
9
|
+
<span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#8e44ad"></span>Évènement</span>
|
|
10
|
+
<span class="onekite-legend-item"><span class="onekite-legend-dot" style="background:#2980b9"></span>Prévision de sortie</span>
|
|
11
|
+
</div>
|
|
5
12
|
<div id="onekite-calendar" style="margin-top: 1rem;"></div>
|
|
6
13
|
</div>
|
|
7
14
|
</div>
|
|
@@ -23,6 +30,29 @@
|
|
|
23
30
|
|
|
24
31
|
|
|
25
32
|
<style>
|
|
33
|
+
.onekite-calendar-legend {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
gap: .5rem .75rem;
|
|
37
|
+
align-items: center;
|
|
38
|
+
font-size: .9rem;
|
|
39
|
+
}
|
|
40
|
+
.onekite-legend-item {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: .35rem;
|
|
44
|
+
padding: .15rem .4rem;
|
|
45
|
+
border-radius: 999px;
|
|
46
|
+
background: rgba(0,0,0,.04);
|
|
47
|
+
}
|
|
48
|
+
.onekite-legend-dot {
|
|
49
|
+
width: .7rem;
|
|
50
|
+
height: .7rem;
|
|
51
|
+
border-radius: 999px;
|
|
52
|
+
display: inline-block;
|
|
53
|
+
border: 1px solid rgba(0,0,0,.15);
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
/* Make the custom "Évènement" button distinct from view buttons */
|
|
27
57
|
.fc .fc-newSpecial-button {
|
|
28
58
|
background: #8e44ad;
|
|
@@ -36,6 +66,7 @@
|
|
|
36
66
|
/* Mobile tweaks for FullCalendar */
|
|
37
67
|
@media (max-width: 576px) {
|
|
38
68
|
#onekite-calendar { margin-top: .5rem !important; }
|
|
69
|
+
.onekite-calendar-legend { font-size: .82rem; }
|
|
39
70
|
.fc .fc-toolbar { flex-wrap: wrap; gap: .25rem; }
|
|
40
71
|
.fc .fc-toolbar-title { font-size: 1.05rem; line-height: 1.2; }
|
|
41
72
|
.fc .fc-button { padding: .25rem .45rem; font-size: .75rem; }
|