nodebb-plugin-equipment-calendar 6.0.0 → 8.0.2
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/library.js +130 -68
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/js/client.js +28 -104
- package/public/templates/equipment-calendar/calendar.tpl +1 -10
package/library.js
CHANGED
|
@@ -27,6 +27,24 @@ function generateId() {
|
|
|
27
27
|
return String(Date.now()) + '-' + Math.random().toString(16).slice(2);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function normalizeItemIds(itemIdsRaw) {
|
|
33
|
+
if (!itemIdsRaw) return [];
|
|
34
|
+
if (Array.isArray(itemIdsRaw)) {
|
|
35
|
+
return itemIdsRaw.map(String).flatMap((v) => String(v).split(',')).map(s => s.trim()).filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
if (typeof itemIdsRaw === 'string') {
|
|
38
|
+
return itemIdsRaw.split(',').map(s => s.trim()).filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
// fallback: single value (number/object/etc.)
|
|
41
|
+
try {
|
|
42
|
+
return [String(itemIdsRaw).trim()].filter(Boolean);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
const axios = require('axios');
|
|
31
49
|
const { DateTime } = require('luxon');
|
|
32
50
|
const { v4: uuidv4 } = require('uuid');
|
|
@@ -256,21 +274,15 @@ async function fetchHelloAssoItems(settings) {
|
|
|
256
274
|
|
|
257
275
|
function pushItem(it, tierName) {
|
|
258
276
|
if (!it) return;
|
|
259
|
-
const id = String(it.id || it.itemId || it.
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
else if (typeof amount === 'string') {
|
|
269
|
-
const s = amount.replace(',', '.').replace(/[^0-9.\-]/g, '');
|
|
270
|
-
price = parseFloat(s) || 0;
|
|
271
|
-
}
|
|
272
|
-
if (!id) return;
|
|
273
|
-
out.push({ id, name, price, location: '' });
|
|
277
|
+
const id = String(it.id || it.itemId || it.reference || it.slug || it.code || it.name || '').trim();
|
|
278
|
+
const name = String(it.name || it.label || it.title || '').trim() || (tierName ? String(tierName) : id);
|
|
279
|
+
if (!id || !name) return;
|
|
280
|
+
const amount = it.amount || it.price || it.unitPrice || it.totalAmount || it.initialAmount;
|
|
281
|
+
const priceCents = (typeof amount === 'number' ? amount : parseInt(amount, 10)) || 0;
|
|
282
|
+
const raw = (typeof priceCents === 'number' ? priceCents : parseInt(priceCents, 10)) || 0;
|
|
283
|
+
// Heuristic: HelloAsso amounts are often in cents; convert when it looks like cents.
|
|
284
|
+
const price = (raw >= 1000 && raw % 100 === 0) ? (raw / 100) : raw;
|
|
285
|
+
out.push({ id, name, price, priceRaw: raw });
|
|
274
286
|
}
|
|
275
287
|
|
|
276
288
|
// Try a few known layouts
|
|
@@ -307,7 +319,7 @@ async function getActiveItems() {
|
|
|
307
319
|
const items = (Array.isArray(rawItems) ? rawItems : []).map((it) => ({
|
|
308
320
|
id: String(it.id || '').trim(),
|
|
309
321
|
name: String(it.name || '').trim(),
|
|
310
|
-
price: Number(
|
|
322
|
+
price: (Number(it.price || 0) || ((Number(it.priceCents || 0) || 0) >= 1000 && (Number(it.priceCents||0)%100===0) ? (Number(it.priceCents||0)/100) : (Number(it.priceCents||0)||0)) || 0),
|
|
311
323
|
active: true,
|
|
312
324
|
})).filter(it => it.id && it.name);
|
|
313
325
|
return items;
|
|
@@ -322,10 +334,10 @@ async function getSettings() {
|
|
|
322
334
|
// Keys:
|
|
323
335
|
// item hash: equipmentCalendar:items (stored in settings as JSON)
|
|
324
336
|
// reservations stored as objects in db, indexed by id, and by itemId
|
|
325
|
-
// reservation object key: equipmentCalendar:
|
|
337
|
+
// reservation object key: equipmentCalendar:reservation:<rid>
|
|
326
338
|
// index by item: equipmentCalendar:item:<itemId>:res (sorted set score=startMillis, value=resId)
|
|
327
339
|
|
|
328
|
-
function resKey(
|
|
340
|
+
function resKey(rid) { return `equipmentCalendar:reservation:${rid}`; }
|
|
329
341
|
function itemIndexKey(itemId) { return `equipmentCalendar:item:${itemId}:res`; }
|
|
330
342
|
|
|
331
343
|
function statusBlocksItem(status) {
|
|
@@ -354,18 +366,19 @@ async function getBookingRids(bookingId) {
|
|
|
354
366
|
return await db.getSetMembers(`equipmentCalendar:booking:${bookingId}:rids`) || [];
|
|
355
367
|
}
|
|
356
368
|
|
|
357
|
-
async function saveReservation(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await db.
|
|
369
|
+
async function saveReservation(r) {
|
|
370
|
+
const rid = String(r.rid || r.id || '');
|
|
371
|
+
if (!rid) throw new Error('missing rid');
|
|
372
|
+
r.rid = rid;
|
|
373
|
+
await db.setObject(resKey(rid), r);
|
|
374
|
+
await db.sortedSetAdd(itemIndexKey(r.itemId), r.startMs, rid);
|
|
375
|
+
await db.sortedSetAdd('equipmentCalendar:reservations', r.startMs, rid);
|
|
362
376
|
}
|
|
363
377
|
|
|
364
|
-
async function getReservation(
|
|
365
|
-
const
|
|
366
|
-
if (!
|
|
367
|
-
|
|
368
|
-
return normalizeReservation(obj);
|
|
378
|
+
async function getReservation(rid) {
|
|
379
|
+
const id = String(rid || '');
|
|
380
|
+
if (!id) return null;
|
|
381
|
+
return await db.getObject(resKey(id));
|
|
369
382
|
}
|
|
370
383
|
|
|
371
384
|
function normalizeReservation(obj) {
|
|
@@ -551,22 +564,28 @@ function verifyWebhook(req, secret) {
|
|
|
551
564
|
|
|
552
565
|
// --- Rendering helpers ---
|
|
553
566
|
function toEvent(res, item, requesterName, canSeeRequester) {
|
|
554
|
-
const start = DateTime.fromMillis(res.startMs).
|
|
555
|
-
const end = DateTime.fromMillis(res.endMs).
|
|
567
|
+
const start = DateTime.fromMillis(res.startMs).toISO();
|
|
568
|
+
const end = DateTime.fromMillis(res.endMs).toISO();
|
|
569
|
+
|
|
570
|
+
let icon = '⏳';
|
|
556
571
|
let className = 'ec-status-pending';
|
|
557
|
-
if (res.status === 'approved_waiting_payment'
|
|
558
|
-
if (res.status === 'paid_validated'
|
|
559
|
-
if (res.status === 'rejected' || res.status === 'cancelled') { className = 'ec-status-
|
|
560
|
-
|
|
572
|
+
if (res.status === 'approved_waiting_payment') { icon = '💳'; className = 'ec-status-awaitpay'; }
|
|
573
|
+
if (res.status === 'paid_validated') { icon = '✅'; className = 'ec-status-valid'; }
|
|
574
|
+
if (res.status === 'rejected' || res.status === 'cancelled') { icon = '❌'; className = 'ec-status-cancel'; }
|
|
575
|
+
|
|
576
|
+
const titleParts = [icon, item ? item.name : res.itemId];
|
|
561
577
|
if (canSeeRequester && requesterName) titleParts.push(`- ${requesterName}`);
|
|
562
578
|
return {
|
|
563
579
|
id: res.id,
|
|
564
580
|
title: titleParts.join(' '),
|
|
565
581
|
start,
|
|
566
582
|
end,
|
|
567
|
-
allDay:
|
|
568
|
-
|
|
569
|
-
extendedProps: {
|
|
583
|
+
allDay: false,
|
|
584
|
+
className,
|
|
585
|
+
extendedProps: {
|
|
586
|
+
status: res.status,
|
|
587
|
+
itemId: res.itemId,
|
|
588
|
+
},
|
|
570
589
|
};
|
|
571
590
|
}
|
|
572
591
|
|
|
@@ -1175,47 +1194,61 @@ async function handleCreateReservation(req, res) {
|
|
|
1175
1194
|
return helpers.notAllowed(req, res);
|
|
1176
1195
|
}
|
|
1177
1196
|
|
|
1178
|
-
const itemIdsRaw = req.body.itemIds
|
|
1179
|
-
const itemIds = (
|
|
1180
|
-
|
|
1181
|
-
.map(s => String(s).trim())
|
|
1182
|
-
.filter(Boolean);
|
|
1197
|
+
const itemIdsRaw = (req.body.itemIds !== undefined ? req.body.itemIds : req.body.itemId);
|
|
1198
|
+
const itemIds = normalizeItemIds(itemIdsRaw);
|
|
1199
|
+
if (!itemIds.length) return res.status(400).send('itemIds required');
|
|
1183
1200
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1201
|
+
// Dates come from modal as YYYY-MM-DD
|
|
1202
|
+
const startStr = String(req.body.start || req.body.startDate || req.body.startIso || '');
|
|
1203
|
+
const endStr = String(req.body.end || req.body.endDate || req.body.endIso || '');
|
|
1204
|
+
if (!startStr || !endStr) return res.status(400).send('dates required');
|
|
1187
1205
|
|
|
1188
|
-
const
|
|
1189
|
-
const
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1206
|
+
const start = new Date(startStr + 'T00:00:00Z');
|
|
1207
|
+
const end = new Date(endStr + 'T00:00:00Z');
|
|
1208
|
+
const startMs = Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate());
|
|
1209
|
+
let endMs = Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate());
|
|
1210
|
+
if (endMs <= startMs) endMs = startMs + 24 * 60 * 60 * 1000;
|
|
1192
1211
|
|
|
1193
|
-
|
|
1194
|
-
const startDay = start.startOf('day');
|
|
1195
|
-
let endDay = end.startOf('day');
|
|
1196
|
-
if (endDay <= startDay) {
|
|
1197
|
-
endDay = startDay.plus({ days: 1 });
|
|
1198
|
-
}
|
|
1212
|
+
const days = Math.max(1, Math.round((endMs - startMs) / (24 * 60 * 60 * 1000)));
|
|
1199
1213
|
|
|
1200
|
-
const
|
|
1201
|
-
const
|
|
1214
|
+
const items = await getActiveItems(settings);
|
|
1215
|
+
const byId = {};
|
|
1216
|
+
items.forEach((it) => { byId[String(it.id)] = it; });
|
|
1202
1217
|
|
|
1203
|
-
const
|
|
1218
|
+
const validIds = itemIds.map(String).filter((id) => byId[id]);
|
|
1219
|
+
if (!validIds.length) return res.status(400).send('unknown item');
|
|
1204
1220
|
|
|
1205
|
-
const
|
|
1221
|
+
const notesUser = String(req.body.notesUser || '').trim();
|
|
1206
1222
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1223
|
+
for (const itemId of validIds) {
|
|
1224
|
+
const it = byId[itemId];
|
|
1225
|
+
const unitPrice = Number(it.price || 0) || 0;
|
|
1226
|
+
const total = unitPrice * days;
|
|
1227
|
+
|
|
1228
|
+
const rid = generateId();
|
|
1229
|
+
const r = {
|
|
1230
|
+
rid,
|
|
1231
|
+
uid: req.uid,
|
|
1232
|
+
itemId,
|
|
1233
|
+
startMs,
|
|
1234
|
+
endMs,
|
|
1235
|
+
startIso: new Date(startMs).toISOString().slice(0, 10),
|
|
1236
|
+
endIso: new Date(endMs).toISOString().slice(0, 10),
|
|
1237
|
+
days,
|
|
1238
|
+
unitPrice,
|
|
1239
|
+
total,
|
|
1240
|
+
notesUser,
|
|
1241
|
+
status: 'pending',
|
|
1242
|
+
createdAt: Date.now(),
|
|
1243
|
+
};
|
|
1244
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1245
|
+
await saveReservation(r);
|
|
1212
1246
|
}
|
|
1213
1247
|
|
|
1214
|
-
|
|
1215
|
-
return res.redirect('/equipment/calendar?requested=1');
|
|
1248
|
+
return res.redirect(nconf.get('relative_path') + '/calendar?created=1');
|
|
1216
1249
|
} catch (err) {
|
|
1217
|
-
winston.error(err);
|
|
1218
|
-
return res.status(500).send(
|
|
1250
|
+
winston.error('[equipment-calendar] create error', err);
|
|
1251
|
+
return res.status(500).send('error');
|
|
1219
1252
|
}
|
|
1220
1253
|
}
|
|
1221
1254
|
|
|
@@ -1464,3 +1497,32 @@ function startPaymentTimeoutScheduler() {
|
|
|
1464
1497
|
}
|
|
1465
1498
|
|
|
1466
1499
|
module.exports = plugin;
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
async function handleGetReservation(req, res) {
|
|
1503
|
+
if (!req.uid) return res.status(403).json({ error: 'not-logged-in' });
|
|
1504
|
+
const rid = String(req.params.id || '');
|
|
1505
|
+
const r = await db.getObject(resKey(rid));
|
|
1506
|
+
if (!r || !r.id) return res.status(404).json({ error: 'not-found' });
|
|
1507
|
+
|
|
1508
|
+
const settings = await getSettings();
|
|
1509
|
+
const isAdmin = await user.isAdminOrGlobalMod(req.uid);
|
|
1510
|
+
const isOwner = String(r.uid) === String(req.uid);
|
|
1511
|
+
const canSee = isAdmin || isOwner || String(settings.showRequesterToAll || '0') === '1';
|
|
1512
|
+
if (!canSee) return res.status(403).json({ error: 'forbidden' });
|
|
1513
|
+
|
|
1514
|
+
res.json({
|
|
1515
|
+
id: r.id,
|
|
1516
|
+
bookingId: r.bookingId || '',
|
|
1517
|
+
itemId: r.itemId,
|
|
1518
|
+
itemName: r.itemName || '',
|
|
1519
|
+
startMs: parseInt(r.startMs, 10) || 0,
|
|
1520
|
+
endMs: parseInt(r.endMs, 10) || 0,
|
|
1521
|
+
days: parseInt(r.days, 10) || 1,
|
|
1522
|
+
status: r.status || 'pending',
|
|
1523
|
+
total: Number(r.total || 0) || 0,
|
|
1524
|
+
unitPrice: Number(r.unitPrice || 0) || 0,
|
|
1525
|
+
notesUser: r.notesUser || '',
|
|
1526
|
+
uid: r.uid,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/js/client.js
CHANGED
|
@@ -29,108 +29,32 @@ require(['jquery', 'bootstrap'], function ($, bootstrap) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function updateTotalPrice() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const endMsExclusive = parseDateInputToMs(e) + 24 * 60 * 60 * 1000;
|
|
61
|
-
startIsoEl.value = new Date(startMs).toISOString();
|
|
62
|
-
endIsoEl.value = new Date(endMsExclusive).toISOString();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function openModalWithRange(startMs, endMsExclusive) {
|
|
66
|
-
// Convert to date inputs: end date = endExclusive -1 day
|
|
67
|
-
const startDate = toIsoDateUTC(startMs);
|
|
68
|
-
const endDate = toIsoDateUTC(endMsExclusive - 24 * 60 * 60 * 1000);
|
|
69
|
-
|
|
70
|
-
const startEl = document.getElementById('ec-start-date');
|
|
71
|
-
const endEl = document.getElementById('ec-end-date');
|
|
72
|
-
if (startEl) startEl.value = startDate;
|
|
73
|
-
if (endEl) endEl.value = endDate;
|
|
74
|
-
|
|
75
|
-
syncHiddenIsoFields();
|
|
76
|
-
updateTotalPrice();
|
|
77
|
-
|
|
78
|
-
const modalEl = document.getElementById('ec-create-modal');
|
|
79
|
-
const BS = (bootstrap && bootstrap.Modal) ? bootstrap : (window.bootstrap && window.bootstrap.Modal ? window.bootstrap : null);
|
|
80
|
-
if (modalEl && BS && BS.Modal) {
|
|
81
|
-
BS.Modal.getOrCreateInstance(modalEl).show();
|
|
82
|
-
} else {
|
|
83
|
-
// Fallback: if bootstrap modal isn't available, scroll to form area
|
|
84
|
-
modalEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function initCalendar() {
|
|
89
|
-
const calendarEl = document.getElementById('ec-calendar');
|
|
90
|
-
if (!calendarEl || !window.FullCalendar) return;
|
|
91
|
-
|
|
92
|
-
const canCreate = !!window.EC_CAN_CREATE;
|
|
93
|
-
const events = Array.isArray(window.EC_EVENTS) ? window.EC_EVENTS : [];
|
|
94
|
-
|
|
95
|
-
const calendar = new window.FullCalendar.Calendar(calendarEl, {
|
|
96
|
-
initialView: 'dayGridMonth',
|
|
97
|
-
locale: 'fr',
|
|
98
|
-
headerToolbar: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,dayGridWeek,listMonth,listWeek' },
|
|
99
|
-
selectable: canCreate,
|
|
100
|
-
selectMirror: true,
|
|
101
|
-
events,
|
|
102
|
-
dateClick: function (info) {
|
|
103
|
-
if (!canCreate) return;
|
|
104
|
-
const startMs = Date.UTC(info.date.getUTCFullYear(), info.date.getUTCMonth(), info.date.getUTCDate());
|
|
105
|
-
const endMs = startMs + 24 * 60 * 60 * 1000;
|
|
106
|
-
console.debug('[equipment-calendar] open modal', { startMs, endMs });
|
|
107
|
-
openModalWithRange(startMs, endMs);
|
|
108
|
-
},
|
|
109
|
-
select: function (info) {
|
|
110
|
-
if (!canCreate) return;
|
|
111
|
-
const startMs = Date.UTC(info.start.getUTCFullYear(), info.start.getUTCMonth(), info.start.getUTCDate());
|
|
112
|
-
const endMs = Date.UTC(info.end.getUTCFullYear(), info.end.getUTCMonth(), info.end.getUTCDate());
|
|
113
|
-
console.debug('[equipment-calendar] open modal', { startMs, endMs });
|
|
114
|
-
openModalWithRange(startMs, endMs);
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
calendar.render();
|
|
119
|
-
|
|
120
|
-
document.getElementById('ec-item-ids')?.addEventListener('change', updateTotalPrice);
|
|
121
|
-
document.getElementById('ec-start-date')?.addEventListener('change', function () {
|
|
122
|
-
syncHiddenIsoFields();
|
|
123
|
-
updateTotalPrice();
|
|
124
|
-
});
|
|
125
|
-
document.getElementById('ec-end-date')?.addEventListener('change', function () {
|
|
126
|
-
syncHiddenIsoFields();
|
|
127
|
-
updateTotalPrice();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
updateTotalPrice();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
$(window).off('action:ajaxify.end.ec').on('action:ajaxify.end.ec', initCalendar);
|
|
134
|
-
$(initCalendar);
|
|
135
|
-
})();
|
|
32
|
+
try {
|
|
33
|
+
const sel = document.getElementById('ec-item-ids');
|
|
34
|
+
const out = document.getElementById('ec-total');
|
|
35
|
+
const startEl = document.getElementById('ec-start');
|
|
36
|
+
const endEl = document.getElementById('ec-end');
|
|
37
|
+
if (!sel || !out || !startEl || !endEl) return;
|
|
38
|
+
|
|
39
|
+
const start = startEl.value ? new Date(startEl.value + 'T00:00:00Z') : null;
|
|
40
|
+
const end = endEl.value ? new Date(endEl.value + 'T00:00:00Z') : null;
|
|
41
|
+
let days = 1;
|
|
42
|
+
if (start && end) {
|
|
43
|
+
const ms = end.getTime() - start.getTime();
|
|
44
|
+
days = Math.max(1, Math.round(ms / (24 * 60 * 60 * 1000)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let sum = 0;
|
|
48
|
+
const opts = Array.from(sel.selectedOptions || []);
|
|
49
|
+
opts.forEach((opt) => {
|
|
50
|
+
const raw = (opt && opt.dataset) ? opt.dataset.price : '';
|
|
51
|
+
const num = Number(String(raw).replace(',', '.'));
|
|
52
|
+
if (!Number.isNaN(num)) sum += num;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const total = sum * days;
|
|
56
|
+
const txt = Number.isInteger(total) ? String(total) : total.toFixed(2);
|
|
57
|
+
out.textContent = txt + ' €';
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
})();
|
|
136
60
|
});
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
<label class="form-label">Matériel</label>
|
|
44
44
|
<select class="form-select" id="ec-item-ids" name="itemIds" multiple required>
|
|
45
45
|
<!-- BEGIN items -->
|
|
46
|
-
<option value="{items.id}" data-price="{items.price}">{items.name}
|
|
46
|
+
<option value="{items.id}" data-price="{items.price}">{items.name}</option>
|
|
47
47
|
<!-- END items -->
|
|
48
48
|
</select>
|
|
49
49
|
<div class="form-text">Tu peux sélectionner plusieurs matériels.</div>
|
|
@@ -82,12 +82,3 @@
|
|
|
82
82
|
window.EC_CAN_CREATE = {canCreateJs};
|
|
83
83
|
window.EC_TZ = '{tz}';
|
|
84
84
|
</script>
|
|
85
|
-
|
|
86
|
-
<style>
|
|
87
|
-
/* Event styling */
|
|
88
|
-
.fc .fc-event-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
89
|
-
.fc-event.ec-status-pending { background: rgba(108,117,125,.18); border-color: rgba(108,117,125,.45); }
|
|
90
|
-
.fc-event.ec-status-approved { background: rgba(13,110,253,.18); border-color: rgba(13,110,253,.45); }
|
|
91
|
-
.fc-event.ec-status-paid { background: rgba(25,135,84,.18); border-color: rgba(25,135,84,.45); }
|
|
92
|
-
.fc-event.ec-status-cancelled { background: rgba(220,53,69,.10); border-color: rgba(220,53,69,.35); opacity:.65; text-decoration: line-through; }
|
|
93
|
-
</style>
|