nodebb-plugin-onekite-calendar 2.0.16 → 2.0.18
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/CHANGELOG.md +13 -0
- package/lib/admin.js +24 -0
- package/lib/scheduler.js +208 -2
- package/package.json +1 -1
- package/pkg/package/lib/admin.js +23 -0
- package/pkg/package/package.json +1 -1
- package/pkg/package/plugin.json +1 -1
- package/pkg/package/public/admin.js +6 -0
- package/plugin.json +1 -1
- package/public/admin.js +6 -0
- package/public/client.js +13 -3
- package/templates/admin/plugins/calendar-onekite.tpl +6 -0
- package/templates/emails/calendar-onekite_expired.tpl +2 -1
- package/templates/emails/calendar-onekite_validator_expired.tpl +17 -0
- package/templates/emails/calendar-onekite_validator_reminder.tpl +16 -0
- package/pkg/nodebb-plugin-onekite-calendar-1.3.9.tgz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog – calendar-onekite
|
|
2
2
|
|
|
3
|
+
## 1.3.12
|
|
4
|
+
- Expiration automatique : envoi d’un email au demandeur avec la raison :
|
|
5
|
+
- « Demande non prise en charge dans le temps imparti » (demande en attente)
|
|
6
|
+
- « Paiement non reçu dans le temps imparti » (paiement en attente)
|
|
7
|
+
- ACP : ajout de 2 paramètres pour envoyer un rappel email aux validateurs sur les demandes en attente / paiement en attente.
|
|
8
|
+
- Emails validateurs : nouveaux templates (rappel + expiration) avec lien vers l’ACP.
|
|
9
|
+
|
|
10
|
+
## 1.3.11
|
|
11
|
+
- ACP (Demandes en attente) : affichage du pseudo du demandeur avec lien vers sa fiche utilisateur.
|
|
12
|
+
|
|
13
|
+
## 1.3.10
|
|
14
|
+
- Mobile FAB : surlignage de la sélection de dates rendu lisible en light mode (contraste renforcé, start/end en primary).
|
|
15
|
+
|
|
3
16
|
## 1.3.9
|
|
4
17
|
- Mobile FAB : correction de la sélection de plage (tous les jours sélectionnés sont désormais bien inclus dans la surbrillance).
|
|
5
18
|
- Mobile FAB : amélioration du rendu en mode sombre (bordures/fonds adaptés, contraste renforcé).
|
package/lib/admin.js
CHANGED
|
@@ -101,6 +101,30 @@ admin.listPending = async function (req, res) {
|
|
|
101
101
|
const rows = await dbLayer.getReservations(ids);
|
|
102
102
|
const pending = (rows || []).filter(r => r && r.status === 'pending');
|
|
103
103
|
pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
|
|
104
|
+
|
|
105
|
+
// Enrich with user info for ACP display (username + userslug)
|
|
106
|
+
try {
|
|
107
|
+
const uids = Array.from(new Set(pending
|
|
108
|
+
.map(r => r && r.uid)
|
|
109
|
+
.map(v => (v ? parseInt(v, 10) : NaN))
|
|
110
|
+
.filter(v => Number.isInteger(v) && v > 0)));
|
|
111
|
+
|
|
112
|
+
if (uids.length) {
|
|
113
|
+
// user.getUsersFields exists in NodeBB 4.x
|
|
114
|
+
const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug']);
|
|
115
|
+
const byUid = new Map((users || []).filter(Boolean).map(u => [String(u.uid), u]));
|
|
116
|
+
for (const r of pending) {
|
|
117
|
+
const u = byUid.get(String(r.uid));
|
|
118
|
+
if (u) {
|
|
119
|
+
r.username = u.username || r.username || '';
|
|
120
|
+
r.userslug = u.userslug || r.userslug || '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Best-effort only; keep API working even if user lookups fail
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
res.json(pending);
|
|
105
129
|
};
|
|
106
130
|
|
package/lib/scheduler.js
CHANGED
|
@@ -4,6 +4,8 @@ const meta = require.main.require('./src/meta');
|
|
|
4
4
|
const db = require.main.require('./src/database');
|
|
5
5
|
const dbLayer = require('./db');
|
|
6
6
|
const discord = require('./discord');
|
|
7
|
+
const nconf = require.main.require('nconf');
|
|
8
|
+
const groups = require.main.require('./src/groups');
|
|
7
9
|
|
|
8
10
|
let timer = null;
|
|
9
11
|
|
|
@@ -14,12 +16,136 @@ function getSetting(settings, key, fallback) {
|
|
|
14
16
|
return v;
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
function formatFR(ts) {
|
|
20
|
+
const d = new Date(ts);
|
|
21
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
22
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
23
|
+
const yyyy = d.getFullYear();
|
|
24
|
+
return `${dd}/${mm}/${yyyy}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function forumBaseUrl() {
|
|
28
|
+
const base = String(nconf.get('url') || '').trim().replace(/\/$/, '');
|
|
29
|
+
return base;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Resolve group identifiers from ACP (name or slug) and return UIDs.
|
|
33
|
+
async function getMembersByGroupIdentifier(groupIdentifier) {
|
|
34
|
+
const id = String(groupIdentifier || '').trim();
|
|
35
|
+
if (!id) return [];
|
|
36
|
+
|
|
37
|
+
let members = [];
|
|
38
|
+
try {
|
|
39
|
+
members = await groups.getMembers(id, 0, -1);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
members = [];
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(members) && members.length) return members;
|
|
44
|
+
|
|
45
|
+
if (typeof groups.getGroupNameByGroupSlug === 'function') {
|
|
46
|
+
let groupName = null;
|
|
47
|
+
try {
|
|
48
|
+
if (groups.getGroupNameByGroupSlug.length >= 2) {
|
|
49
|
+
groupName = await new Promise((resolve) => {
|
|
50
|
+
groups.getGroupNameByGroupSlug(id, (err, name) => resolve(err ? null : name));
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
groupName = await groups.getGroupNameByGroupSlug(id);
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
groupName = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (groupName && String(groupName).trim() && String(groupName).trim() !== id) {
|
|
60
|
+
try {
|
|
61
|
+
members = await groups.getMembers(String(groupName).trim(), 0, -1);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
members = [];
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(members) && members.length) return members;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Array.isArray(members) ? members : [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeUids(members) {
|
|
73
|
+
if (!Array.isArray(members)) return [];
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const m of members) {
|
|
76
|
+
if (Number.isInteger(m)) { out.push(m); continue; }
|
|
77
|
+
if (typeof m === 'string' && m.trim() && !Number.isNaN(parseInt(m, 10))) { out.push(parseInt(m, 10)); continue; }
|
|
78
|
+
if (m && typeof m === 'object' && (Number.isInteger(m.uid) || (typeof m.uid === 'string' && m.uid.trim()))) {
|
|
79
|
+
const u = Number.isInteger(m.uid) ? m.uid : parseInt(m.uid, 10);
|
|
80
|
+
if (!Number.isNaN(u)) out.push(u);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return Array.from(new Set(out)).filter((u) => Number.isInteger(u) && u > 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeAllowedGroups(raw) {
|
|
87
|
+
if (!raw) return [];
|
|
88
|
+
if (Array.isArray(raw)) {
|
|
89
|
+
return raw.map((s) => String(s || '').trim()).filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
return String(raw)
|
|
92
|
+
.split(',')
|
|
93
|
+
.map((s) => String(s || '').trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function getValidatorUids(settings) {
|
|
98
|
+
const out = new Set();
|
|
99
|
+
// Always include administrators
|
|
100
|
+
try {
|
|
101
|
+
const admins = await getMembersByGroupIdentifier('administrators');
|
|
102
|
+
normalizeUids(admins).forEach((u) => out.add(u));
|
|
103
|
+
} catch (e) {}
|
|
104
|
+
|
|
105
|
+
const groupsCsv = normalizeAllowedGroups(settings && settings.validatorGroups);
|
|
106
|
+
for (const g of groupsCsv) {
|
|
107
|
+
try {
|
|
108
|
+
const members = await getMembersByGroupIdentifier(g);
|
|
109
|
+
normalizeUids(members).forEach((u) => out.add(u));
|
|
110
|
+
} catch (e) {}
|
|
111
|
+
}
|
|
112
|
+
return Array.from(out);
|
|
113
|
+
}
|
|
114
|
+
|
|
17
115
|
// Pending holds: short lock after a user creates a request (defaults to 5 minutes)
|
|
18
116
|
async function expirePending() {
|
|
19
117
|
const settings = await meta.settings.get('calendar-onekite');
|
|
20
118
|
const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
|
|
119
|
+
const validatorReminderMins = parseInt(getSetting(settings, 'validatorReminderMinutesPending', '0'), 10) || 0;
|
|
21
120
|
const now = Date.now();
|
|
22
121
|
|
|
122
|
+
const emailer = require.main.require('./src/emailer');
|
|
123
|
+
const user = require.main.require('./src/user');
|
|
124
|
+
|
|
125
|
+
async function sendEmail(template, uid, subject, data) {
|
|
126
|
+
const toUid = Number.isInteger(uid) ? uid : (uid ? parseInt(uid, 10) : NaN);
|
|
127
|
+
if (!Number.isInteger(toUid) || toUid <= 0) return;
|
|
128
|
+
const params = Object.assign({}, data || {}, subject ? { subject } : {});
|
|
129
|
+
try {
|
|
130
|
+
if (typeof emailer.send !== 'function') return;
|
|
131
|
+
await emailer.send(template, toUid, params);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.warn('[calendar-onekite] Failed to send email (scheduler)', {
|
|
135
|
+
template,
|
|
136
|
+
uid: toUid,
|
|
137
|
+
err: String((err && err.message) || err),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const adminUrl = (() => {
|
|
143
|
+
const base = forumBaseUrl();
|
|
144
|
+
return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
|
|
145
|
+
})();
|
|
146
|
+
|
|
147
|
+
const validatorUids = validatorReminderMins > 0 ? await getValidatorUids(settings) : [];
|
|
148
|
+
|
|
23
149
|
const ids = await dbLayer.listAllReservationIds(5000);
|
|
24
150
|
if (!ids || !ids.length) {
|
|
25
151
|
return;
|
|
@@ -32,8 +158,66 @@ async function expirePending() {
|
|
|
32
158
|
}
|
|
33
159
|
const createdAt = parseInt(resv.createdAt, 10) || 0;
|
|
34
160
|
const expiresAt = createdAt + holdMins * 60 * 1000;
|
|
161
|
+
|
|
162
|
+
// Reminder to validators while still pending
|
|
163
|
+
if (validatorReminderMins > 0 && createdAt && now >= (createdAt + validatorReminderMins * 60 * 1000) && now < expiresAt) {
|
|
164
|
+
const reminderKey = 'calendar-onekite:email:validatorReminder:pending';
|
|
165
|
+
const first = await db.setAdd(reminderKey, rid);
|
|
166
|
+
if (first && validatorUids.length) {
|
|
167
|
+
const requesterUid = parseInt(resv.uid, 10) || 0;
|
|
168
|
+
const requester = requesterUid ? await user.getUserFields(requesterUid, ['username']) : null;
|
|
169
|
+
const requesterUsername = requester && requester.username ? requester.username : (resv.username || '');
|
|
170
|
+
for (const vuid of validatorUids) {
|
|
171
|
+
await sendEmail('calendar-onekite_validator_reminder', vuid, 'Location matériel - Demande en attente', {
|
|
172
|
+
rid,
|
|
173
|
+
requesterUsername,
|
|
174
|
+
itemNames: (Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : [])).filter(Boolean),
|
|
175
|
+
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
176
|
+
adminUrl,
|
|
177
|
+
kind: 'pending',
|
|
178
|
+
delayMinutes: validatorReminderMins,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
35
184
|
if (now > expiresAt) {
|
|
36
|
-
// Expire (remove from calendar)
|
|
185
|
+
// Expire (remove from calendar) + notify requester + validators
|
|
186
|
+
const expiredKey = 'calendar-onekite:email:expiredSent:pending';
|
|
187
|
+
const firstExpired = await db.setAdd(expiredKey, rid);
|
|
188
|
+
|
|
189
|
+
const requesterUid = parseInt(resv.uid, 10) || 0;
|
|
190
|
+
const requester = requesterUid ? await user.getUserFields(requesterUid, ['username']) : null;
|
|
191
|
+
const requesterUsername = requester && requester.username ? requester.username : (resv.username || '');
|
|
192
|
+
|
|
193
|
+
const reason = 'Demande non prise en charge dans le temps imparti.';
|
|
194
|
+
|
|
195
|
+
if (firstExpired && requesterUid) {
|
|
196
|
+
await sendEmail('calendar-onekite_expired', requesterUid, 'Location matériel - Demande expirée', {
|
|
197
|
+
uid: requesterUid,
|
|
198
|
+
username: requesterUsername,
|
|
199
|
+
itemNames: (Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : [])).filter(Boolean),
|
|
200
|
+
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
201
|
+
delayMinutes: holdMins,
|
|
202
|
+
reason,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Validators info email (best-effort)
|
|
207
|
+
if (firstExpired) {
|
|
208
|
+
const validators = await getValidatorUids(settings);
|
|
209
|
+
for (const vuid of validators) {
|
|
210
|
+
await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
|
|
211
|
+
rid,
|
|
212
|
+
requesterUsername,
|
|
213
|
+
itemNames: (Array.isArray(resv.itemNames) ? resv.itemNames : (resv.itemName ? [resv.itemName] : [])).filter(Boolean),
|
|
214
|
+
dateRange: `Du ${formatFR(resv.start)} au ${formatFR(resv.end)}`,
|
|
215
|
+
adminUrl,
|
|
216
|
+
reason,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
37
221
|
await dbLayer.removeReservation(rid);
|
|
38
222
|
}
|
|
39
223
|
}
|
|
@@ -87,6 +271,11 @@ async function processAwaitingPayment() {
|
|
|
87
271
|
return `${dd}/${mm}/${yyyy}`;
|
|
88
272
|
}
|
|
89
273
|
|
|
274
|
+
const adminUrl = (() => {
|
|
275
|
+
const base = forumBaseUrl();
|
|
276
|
+
return base ? `${base}/admin/plugins/calendar-onekite` : '/admin/plugins/calendar-onekite';
|
|
277
|
+
})();
|
|
278
|
+
|
|
90
279
|
for (const rid of ids) {
|
|
91
280
|
const r = await dbLayer.getReservation(rid);
|
|
92
281
|
if (!r || r.status !== 'awaiting_payment') continue;
|
|
@@ -146,16 +335,33 @@ async function processAwaitingPayment() {
|
|
|
146
335
|
const u = await user.getUserFields(toUid, ['username']);
|
|
147
336
|
|
|
148
337
|
if (shouldEmail && toUid) {
|
|
149
|
-
await sendEmail('calendar-onekite_expired', toUid, 'Location matériel -
|
|
338
|
+
await sendEmail('calendar-onekite_expired', toUid, 'Location matériel - Demande expirée', {
|
|
150
339
|
uid: toUid,
|
|
151
340
|
username: (u && u.username) ? u.username : '',
|
|
152
341
|
itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
|
|
153
342
|
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
|
|
154
343
|
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
155
344
|
delayMinutes: holdMins,
|
|
345
|
+
reason: 'Paiement non reçu dans le temps imparti.',
|
|
156
346
|
});
|
|
157
347
|
}
|
|
158
348
|
|
|
349
|
+
// Validators info email (best-effort)
|
|
350
|
+
if (shouldEmail) {
|
|
351
|
+
const validators = await getValidatorUids(settings);
|
|
352
|
+
for (const vuid of validators) {
|
|
353
|
+
await sendEmail('calendar-onekite_validator_expired', vuid, 'Location matériel - Demande expirée', {
|
|
354
|
+
rid: r.rid || rid,
|
|
355
|
+
requesterUsername: (u && u.username) ? u.username : (r.username || ''),
|
|
356
|
+
itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
|
|
357
|
+
dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
|
|
358
|
+
adminUrl,
|
|
359
|
+
reason: 'Paiement non reçu dans le temps imparti.',
|
|
360
|
+
paymentUrl: r.paymentUrl || '',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
159
365
|
if (shouldDiscord) {
|
|
160
366
|
try {
|
|
161
367
|
await discord.notifyReservationCancelled(settings, {
|
package/package.json
CHANGED
package/pkg/package/lib/admin.js
CHANGED
|
@@ -101,6 +101,29 @@ admin.listPending = async function (req, res) {
|
|
|
101
101
|
const rows = await dbLayer.getReservations(ids);
|
|
102
102
|
const pending = (rows || []).filter(r => r && r.status === 'pending');
|
|
103
103
|
pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
|
|
104
|
+
|
|
105
|
+
// Enrich with user info for ACP display (username + userslug)
|
|
106
|
+
try {
|
|
107
|
+
const uids = Array.from(new Set(pending
|
|
108
|
+
.map(r => r && r.uid)
|
|
109
|
+
.map(v => (v ? parseInt(v, 10) : NaN))
|
|
110
|
+
.filter(v => Number.isInteger(v) && v > 0)));
|
|
111
|
+
|
|
112
|
+
if (uids.length) {
|
|
113
|
+
const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug']);
|
|
114
|
+
const byUid = new Map((users || []).filter(Boolean).map(u => [String(u.uid), u]));
|
|
115
|
+
for (const r of pending) {
|
|
116
|
+
const u = byUid.get(String(r.uid));
|
|
117
|
+
if (u) {
|
|
118
|
+
r.username = u.username || r.username || '';
|
|
119
|
+
r.userslug = u.userslug || r.userslug || '';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Best-effort only
|
|
125
|
+
}
|
|
126
|
+
|
|
104
127
|
res.json(pending);
|
|
105
128
|
};
|
|
106
129
|
|
package/pkg/package/package.json
CHANGED
package/pkg/package/plugin.json
CHANGED
|
@@ -458,6 +458,11 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
458
458
|
for (const r of list) {
|
|
459
459
|
if (r && r.rid) pendingCache.set(String(r.rid), r);
|
|
460
460
|
const created = r.createdAt ? fmtFR(r.createdAt) : '';
|
|
461
|
+
const userSlug = (r && (r.userslug || r.username)) ? String(r.userslug || r.username) : '';
|
|
462
|
+
const userName = (r && r.username) ? String(r.username) : '';
|
|
463
|
+
const userLink = (userSlug && userName)
|
|
464
|
+
? `<a href="${escapeHtml('/user/' + userSlug)}" target="_blank" rel="noopener">${escapeHtml(userName)}</a>`
|
|
465
|
+
: '';
|
|
461
466
|
const itemNames = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
|
|
462
467
|
const itemsHtml = `<ul style="margin: 0 0 10px 18px;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
|
|
463
468
|
const div = document.createElement('div');
|
|
@@ -471,6 +476,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
471
476
|
<div style="min-width:0;">
|
|
472
477
|
<div><strong>${itemsHtml || escapeHtml(r.itemName || '')}</strong></div>
|
|
473
478
|
<div class="text-muted" style="font-size: 12px;">Créée: ${escapeHtml(created)}</div>
|
|
479
|
+
${userLink ? `<div class="text-muted" style="font-size: 12px;">Réservée par: ${userLink}</div>` : ''}
|
|
474
480
|
<div class="text-muted" style="font-size: 12px;">Période: ${escapeHtml(new Date(parseInt(r.start, 10)).toLocaleDateString('fr-FR'))} → ${escapeHtml(new Date(parseInt(r.end, 10)).toLocaleDateString('fr-FR'))}</div>
|
|
475
481
|
</div>
|
|
476
482
|
</div>
|
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -458,6 +458,11 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
458
458
|
for (const r of list) {
|
|
459
459
|
if (r && r.rid) pendingCache.set(String(r.rid), r);
|
|
460
460
|
const created = r.createdAt ? fmtFR(r.createdAt) : '';
|
|
461
|
+
const userSlug = (r && (r.userslug || r.username)) ? String(r.userslug || r.username) : '';
|
|
462
|
+
const userName = (r && r.username) ? String(r.username) : '';
|
|
463
|
+
const userLink = (userSlug && userName)
|
|
464
|
+
? `<a href="${escapeHtml('/user/' + userSlug)}" target="_blank" rel="noopener">${escapeHtml(userName)}</a>`
|
|
465
|
+
: '';
|
|
461
466
|
const itemNames = Array.isArray(r.itemNames) && r.itemNames.length ? r.itemNames : [r.itemName || r.itemId].filter(Boolean);
|
|
462
467
|
const itemsHtml = `<ul style="margin: 0 0 10px 18px;">${itemNames.map(n => `<li>${escapeHtml(String(n))}</li>`).join('')}</ul>`;
|
|
463
468
|
const div = document.createElement('div');
|
|
@@ -471,6 +476,7 @@ define('admin/plugins/calendar-onekite', ['alerts', 'bootbox'], function (alerts
|
|
|
471
476
|
<div style="min-width:0;">
|
|
472
477
|
<div><strong>${itemsHtml || escapeHtml(r.itemName || '')}</strong></div>
|
|
473
478
|
<div class="text-muted" style="font-size: 12px;">Créée: ${escapeHtml(created)}</div>
|
|
479
|
+
${userLink ? `<div class="text-muted" style="font-size: 12px;">Réservée par: ${userLink}</div>` : ''}
|
|
474
480
|
<div class="text-muted" style="font-size: 12px;">Période: ${escapeHtml(new Date(parseInt(r.start, 10)).toLocaleDateString('fr-FR'))} → ${escapeHtml(new Date(parseInt(r.end, 10)).toLocaleDateString('fr-FR'))}</div>
|
|
475
481
|
</div>
|
|
476
482
|
</div>
|
package/public/client.js
CHANGED
|
@@ -72,14 +72,24 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
|
|
|
72
72
|
.onekite-range-day { border:1px solid var(--bs-border-color, rgba(0,0,0,.18)); border-radius:10px; padding:8px 0; text-align:center; cursor:pointer; background:var(--bs-body-bg,#fff); color:var(--bs-body-color,#212529); }
|
|
73
73
|
.onekite-range-day.is-empty { border-color:transparent; background:transparent; cursor:default; }
|
|
74
74
|
.onekite-range-day.is-disabled { opacity:.35; cursor:not-allowed; }
|
|
75
|
-
.onekite-range-day.is-start, .onekite-range-day.is-end {
|
|
76
|
-
|
|
75
|
+
.onekite-range-day.is-start, .onekite-range-day.is-end {
|
|
76
|
+
/* Use strong, theme-aware contrast so the selection is always visible in light mode */
|
|
77
|
+
border-color: var(--bs-primary, var(--bs-link-color, #0d6efd));
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
background: var(--bs-primary, #0d6efd);
|
|
80
|
+
color: var(--bs-white, #fff);
|
|
81
|
+
}
|
|
82
|
+
.onekite-range-day.is-inrange {
|
|
83
|
+
/* Some NodeBB themes set secondary-bg ~= body-bg, making the range invisible. */
|
|
84
|
+
background: var(--bs-primary-bg-subtle, rgba(13,110,253,.12));
|
|
85
|
+
box-shadow: inset 0 0 0 1px var(--bs-primary-border-subtle, rgba(13,110,253,.25));
|
|
86
|
+
}
|
|
77
87
|
.onekite-range-day:focus { outline: none; }
|
|
78
88
|
|
|
79
89
|
@media (prefers-color-scheme: dark) {
|
|
80
90
|
.onekite-range-day { border-color: var(--bs-border-color, rgba(255,255,255,.18)); }
|
|
81
91
|
.onekite-range-day.is-inrange { background: rgba(255,255,255,.08); }
|
|
82
|
-
.onekite-range-day.is-start, .onekite-range-day.is-end { background: rgba(255,255,255,.14); }
|
|
92
|
+
.onekite-range-day.is-start, .onekite-range-day.is-end { background: rgba(255,255,255,.14); color: var(--bs-body-color,#f8f9fa); border-color: var(--bs-border-color, rgba(255,255,255,.22)); }
|
|
83
93
|
}
|
|
84
94
|
.onekite-range-summary { margin-top:10px; font-size:.9rem; }
|
|
85
95
|
|
|
@@ -53,6 +53,12 @@
|
|
|
53
53
|
<input class="form-control" name="pendingHoldMinutes" placeholder="5">
|
|
54
54
|
</div>
|
|
55
55
|
|
|
56
|
+
<div class="mb-3">
|
|
57
|
+
<label class="form-label">Rappel validateurs (demande en attente) après (minutes)</label>
|
|
58
|
+
<input class="form-control" name="validatorReminderMinutesPending" placeholder="0">
|
|
59
|
+
<div class="form-text">Si > 0, un email de rappel est envoyé aux validateurs après ce délai tant que la demande est toujours en attente.</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
56
62
|
<div class="mb-3">
|
|
57
63
|
<label class="form-label">Délai rappel paiement (minutes)</label>
|
|
58
64
|
<input class="form-control" name="paymentHoldMinutes" placeholder="60">
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<p>Bonjour,</p>
|
|
2
|
+
|
|
3
|
+
<p>Une demande de location de matériel a expiré automatiquement.</p>
|
|
4
|
+
|
|
5
|
+
<p><strong>Demandeur :</strong> {requesterUsername}</p>
|
|
6
|
+
<p><strong>Raison :</strong> {reason}</p>
|
|
7
|
+
|
|
8
|
+
<p><strong>Matériel</strong></p>
|
|
9
|
+
<ul>
|
|
10
|
+
<!-- BEGIN itemNames -->
|
|
11
|
+
<li>{itemNames}</li>
|
|
12
|
+
<!-- END itemNames -->
|
|
13
|
+
</ul>
|
|
14
|
+
|
|
15
|
+
<p>{dateRange}</p>
|
|
16
|
+
|
|
17
|
+
<p><a href="{adminUrl}">Ouvrir l'ACP</a></p>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<p>Bonjour,</p>
|
|
2
|
+
|
|
3
|
+
<p>Une demande de location de matériel nécessite une action.</p>
|
|
4
|
+
|
|
5
|
+
<p><strong>Demandeur :</strong> {requesterUsername}</p>
|
|
6
|
+
|
|
7
|
+
<p><strong>Matériel</strong></p>
|
|
8
|
+
<ul>
|
|
9
|
+
<!-- BEGIN itemNames -->
|
|
10
|
+
<li>{itemNames}</li>
|
|
11
|
+
<!-- END itemNames -->
|
|
12
|
+
</ul>
|
|
13
|
+
|
|
14
|
+
<p>{dateRange}</p>
|
|
15
|
+
|
|
16
|
+
<p><a href="{adminUrl}">Ouvrir l'ACP (Demandes en attente)</a></p>
|
|
Binary file
|