nodebb-plugin-calendar-onekite 11.1.21 → 11.1.23
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 +63 -209
- package/lib/api.js +125 -131
- package/lib/db.js +36 -22
- package/lib/helloasso.js +80 -106
- package/library.js +67 -69
- package/package.json +4 -5
- package/plugin.json +16 -12
- package/public/admin.js +99 -196
- package/public/client.js +186 -123
- package/templates/admin/plugins/calendar-onekite.tpl +74 -77
- package/templates/calendar-onekite.tpl +5 -15
package/lib/admin.js
CHANGED
|
@@ -1,222 +1,76 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const meta = require.main.require('./src/meta');
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const dbi = require('./db');
|
|
5
|
+
const api = require('./api');
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const helloasso = require('./helloasso');
|
|
9
|
-
|
|
10
|
-
const ADMIN_PRIV = 'admin:settings';
|
|
11
|
-
|
|
12
|
-
const admin = {};
|
|
13
|
-
|
|
14
|
-
admin.renderAdmin = async function (req, res) {
|
|
15
|
-
res.render('admin/plugins/calendar-onekite', {
|
|
16
|
-
title: 'Calendar OneKite',
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
admin.getSettings = async function (req, res) {
|
|
21
|
-
const settings = await meta.settings.get('calendar-onekite');
|
|
22
|
-
res.json(settings || {});
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
admin.saveSettings = async function (req, res) {
|
|
26
|
-
await meta.settings.set('calendar-onekite', req.body || {});
|
|
27
|
-
res.json({ ok: true });
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
admin.listPending = async function (req, res) {
|
|
31
|
-
const ids = await dbLayer.listAllReservationIds(5000);
|
|
32
|
-
const pending = [];
|
|
33
|
-
for (const rid of ids) {
|
|
34
|
-
const r = await dbLayer.getReservation(rid);
|
|
35
|
-
if (r && r.status === 'pending') {
|
|
36
|
-
pending.push(r);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
pending.sort((a, b) => parseInt(a.start, 10) - parseInt(b.start, 10));
|
|
40
|
-
res.json(pending);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
admin.approveReservation = async function (req, res) {
|
|
44
|
-
const rid = req.params.rid;
|
|
45
|
-
const r = await dbLayer.getReservation(rid);
|
|
46
|
-
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
47
|
-
|
|
48
|
-
r.status = 'approved';
|
|
49
|
-
|
|
50
|
-
// Create HelloAsso payment link if configured
|
|
7
|
+
async function getSettings(req, res) {
|
|
51
8
|
const settings = await meta.settings.get('calendar-onekite');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const items = await helloasso.listItems({
|
|
66
|
-
env,
|
|
67
|
-
token,
|
|
68
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
69
|
-
formType: settings.helloassoFormType,
|
|
70
|
-
formSlug: settings.helloassoFormSlug,
|
|
71
|
-
});
|
|
72
|
-
const normalized = (items || []).map((it) => ({
|
|
73
|
-
id: String(it.id || it.itemId || it.reference || it.name),
|
|
74
|
-
price: it.price || it.amount || it.unitPrice || 0,
|
|
75
|
-
})).filter(it => it.id);
|
|
76
|
-
const match = normalized.find(it => it.id === String(r.itemId));
|
|
77
|
-
totalAmount = match ? parseInt(match.price, 10) || 0 : 0;
|
|
78
|
-
} catch (e) {
|
|
79
|
-
totalAmount = 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!totalAmount) {
|
|
83
|
-
return res.status(400).json({ error: 'item-price-not-found' });
|
|
84
|
-
}
|
|
85
|
-
paymentUrl = await helloasso.createCheckoutIntent({
|
|
86
|
-
env,
|
|
87
|
-
token,
|
|
88
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
89
|
-
formType: settings.helloassoFormType,
|
|
90
|
-
formSlug: settings.helloassoFormSlug,
|
|
91
|
-
totalAmount,
|
|
92
|
-
payerEmail: requester && requester.email,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (paymentUrl) {
|
|
97
|
-
r.paymentUrl = paymentUrl;
|
|
9
|
+
// mask secret
|
|
10
|
+
if (settings && settings.helloassoClientSecret) settings.helloassoClientSecret = '***';
|
|
11
|
+
res.json({ ok: true, settings: settings || {} });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function saveSettings(req, res) {
|
|
15
|
+
const body = req.body || {};
|
|
16
|
+
// If secret is '***', do not overwrite.
|
|
17
|
+
const current = await meta.settings.get('calendar-onekite') || {};
|
|
18
|
+
const next = { ...current, ...body };
|
|
19
|
+
if (body.helloassoClientSecret === '***') {
|
|
20
|
+
next.helloassoClientSecret = current.helloassoClientSecret;
|
|
98
21
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Email requester
|
|
103
|
-
try {
|
|
104
|
-
const requester = await user.getUserData(r.uid);
|
|
105
|
-
if (requester && requester.email) {
|
|
106
|
-
await emailer.send('calendar-onekite_approved', requester.email, {
|
|
107
|
-
username: requester.username,
|
|
108
|
-
itemName: r.itemName,
|
|
109
|
-
start: new Date(parseInt(r.start, 10)).toISOString(),
|
|
110
|
-
end: new Date(parseInt(r.end, 10)).toISOString(),
|
|
111
|
-
paymentUrl: paymentUrl || '',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
} catch (e) {}
|
|
115
|
-
|
|
116
|
-
res.json({ ok: true, paymentUrl: paymentUrl || null });
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
admin.refuseReservation = async function (req, res) {
|
|
120
|
-
const rid = req.params.rid;
|
|
121
|
-
const r = await dbLayer.getReservation(rid);
|
|
122
|
-
if (!r) return res.status(404).json({ error: 'not-found' });
|
|
123
|
-
|
|
124
|
-
r.status = 'refused';
|
|
125
|
-
await dbLayer.saveReservation(r);
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const requester = await user.getUserData(r.uid);
|
|
129
|
-
if (requester && requester.email) {
|
|
130
|
-
await emailer.send('calendar-onekite_refused', requester.email, {
|
|
131
|
-
username: requester.username,
|
|
132
|
-
itemName: r.itemName,
|
|
133
|
-
start: new Date(parseInt(r.start, 10)).toISOString(),
|
|
134
|
-
end: new Date(parseInt(r.end, 10)).toISOString(),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {}
|
|
138
|
-
|
|
22
|
+
await meta.settings.set('calendar-onekite', next);
|
|
23
|
+
// invalidate catalog cache
|
|
139
24
|
res.json({ ok: true });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getPending(req, res) {
|
|
28
|
+
const ids = await dbi.listAllIds(5000);
|
|
29
|
+
const rows = await dbi.getReservations(ids);
|
|
30
|
+
const pending = rows.filter(r => (r.status || 'pending') === 'pending')
|
|
31
|
+
.sort((a,b)=> (b.createdAt||0)-(a.createdAt||0))
|
|
32
|
+
.slice(0, 200);
|
|
33
|
+
res.json({ ok: true, pending });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function purgeByYear(req, res) {
|
|
37
|
+
const year = String((req.body||{}).year || '').trim();
|
|
38
|
+
if (!/^\d{4}$/.test(year)) return res.status(400).json({ status: { code: 'bad-request', message: 'Année invalide (YYYY)' } });
|
|
39
|
+
const y = Number(year);
|
|
40
|
+
const startTs = Date.UTC(y,0,1,0,0,0);
|
|
41
|
+
const endTs = Date.UTC(y+1,0,1,0,0,0);
|
|
42
|
+
|
|
43
|
+
const ids = await dbi.listReservationIdsByStartRange(startTs, endTs, 100000);
|
|
44
|
+
for (const id of ids) {
|
|
45
|
+
await dbi.deleteReservation(id);
|
|
146
46
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
count++;
|
|
156
|
-
}
|
|
157
|
-
res.json({ ok: true, removed: count });
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
// Debug endpoint to validate HelloAsso connectivity and item loading
|
|
161
|
-
admin.debugHelloAsso = async function (req, res) {
|
|
162
|
-
const settings = await meta.settings.get('calendar-onekite');
|
|
163
|
-
const env = (settings && settings.helloassoEnv) || 'prod';
|
|
164
|
-
|
|
165
|
-
// Never expose secrets in debug output
|
|
166
|
-
const safeSettings = {
|
|
167
|
-
helloassoEnv: env,
|
|
168
|
-
helloassoClientId: settings && settings.helloassoClientId ? String(settings.helloassoClientId) : '',
|
|
169
|
-
helloassoClientSecret: settings && settings.helloassoClientSecret ? '***' : '',
|
|
170
|
-
helloassoOrganizationSlug: settings && settings.helloassoOrganizationSlug ? String(settings.helloassoOrganizationSlug) : '',
|
|
171
|
-
helloassoFormType: settings && settings.helloassoFormType ? String(settings.helloassoFormType) : '',
|
|
172
|
-
helloassoFormSlug: settings && settings.helloassoFormSlug ? String(settings.helloassoFormSlug) : '',
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const out = {
|
|
176
|
-
ok: true,
|
|
177
|
-
settings: safeSettings,
|
|
178
|
-
token: { ok: false },
|
|
179
|
-
items: { ok: false, count: 0, sample: [] },
|
|
180
|
-
};
|
|
181
|
-
|
|
47
|
+
res.json({ ok: true, deleted: ids.length });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function debugHelloAsso(req, res) {
|
|
51
|
+
const settings = await meta.settings.get('calendar-onekite') || {};
|
|
52
|
+
const masked = { ...settings };
|
|
53
|
+
if (masked.helloassoClientSecret) masked.helloassoClientSecret = '***';
|
|
54
|
+
const out = { ok: true, settings: masked };
|
|
182
55
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
clientId: settings.helloassoClientId,
|
|
186
|
-
clientSecret: settings.helloassoClientSecret,
|
|
187
|
-
});
|
|
188
|
-
if (!token) {
|
|
189
|
-
out.token = { ok: false, error: 'token-null' };
|
|
190
|
-
return res.json(out);
|
|
191
|
-
}
|
|
56
|
+
// token + catalog
|
|
57
|
+
const items = await api._getCatalogItems();
|
|
192
58
|
out.token = { ok: true };
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
token,
|
|
198
|
-
organizationSlug: settings.helloassoOrganizationSlug,
|
|
199
|
-
formType: settings.helloassoFormType,
|
|
200
|
-
formSlug: settings.helloassoFormSlug,
|
|
201
|
-
});
|
|
202
|
-
const arr = Array.isArray(items) ? items : [];
|
|
203
|
-
out.items.ok = true;
|
|
204
|
-
out.items.count = arr.length;
|
|
205
|
-
out.items.sample = arr.slice(0, 10).map((it) => ({
|
|
206
|
-
id: it.id || it.itemId || it.reference || it.name,
|
|
207
|
-
name: it.name || it.label || it.itemName,
|
|
208
|
-
price: it.price || it.amount || it.unitPrice || null,
|
|
209
|
-
}));
|
|
210
|
-
} catch (e) {
|
|
211
|
-
out.items = { ok: false, error: String(e && e.message ? e.message : e), count: 0, sample: [] };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return res.json(out);
|
|
59
|
+
out.catalog = { ok: true, count: items.length, sample: items.slice(0, 10) };
|
|
60
|
+
// also expose cache error if any
|
|
61
|
+
const cache = api._catalogCache();
|
|
62
|
+
if (cache && cache.err) out.catalog.err = cache.err;
|
|
215
63
|
} catch (e) {
|
|
216
|
-
out.
|
|
217
|
-
out.
|
|
218
|
-
return res.json(out);
|
|
64
|
+
out.token = { ok: false, message: e.message };
|
|
65
|
+
out.catalog = { ok: false, count: 0, sample: [], message: e.message };
|
|
219
66
|
}
|
|
67
|
+
res.json(out);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
getSettings,
|
|
72
|
+
saveSettings,
|
|
73
|
+
getPending,
|
|
74
|
+
purgeByYear,
|
|
75
|
+
debugHelloAsso,
|
|
220
76
|
};
|
|
221
|
-
|
|
222
|
-
module.exports = admin;
|
package/lib/api.js
CHANGED
|
@@ -1,158 +1,152 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
|
|
5
3
|
const meta = require.main.require('./src/meta');
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const d = new Date(v);
|
|
15
|
-
return d.getTime();
|
|
4
|
+
const dbi = require('./db');
|
|
5
|
+
const hello = require('./helloasso');
|
|
6
|
+
|
|
7
|
+
function parseDateParam(s) {
|
|
8
|
+
// FullCalendar sends ISO date/time. Accept 'YYYY-MM-DD' too.
|
|
9
|
+
const d = new Date(s);
|
|
10
|
+
if (!s || Number.isNaN(d.getTime())) return null;
|
|
11
|
+
return d;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
14
|
+
function toYMD(ts) {
|
|
15
|
+
const d = new Date(Number(ts));
|
|
16
|
+
const y = d.getUTCFullYear();
|
|
17
|
+
const m = String(d.getUTCMonth()+1).padStart(2,'0');
|
|
18
|
+
const da = String(d.getUTCDate()).padStart(2,'0');
|
|
19
|
+
return `${y}-${m}-${da}`;
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
start: new Date(parseInt(resv.start, 10)).toISOString(),
|
|
35
|
-
end: new Date(parseInt(resv.end, 10)).toISOString(),
|
|
36
|
-
extendedProps: {
|
|
37
|
-
status,
|
|
38
|
-
uid: resv.uid,
|
|
39
|
-
itemId: resv.itemId,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
22
|
+
function daysBetweenInclusive(startYMD, endYMDExclusive) {
|
|
23
|
+
// FullCalendar selection end is exclusive. We compute number of days selected.
|
|
24
|
+
const s = new Date(startYMD + 'T00:00:00Z');
|
|
25
|
+
const e = new Date(endYMDExclusive + 'T00:00:00Z');
|
|
26
|
+
const diff = Math.max(0, Math.round((e - s) / 86400000));
|
|
27
|
+
return diff || 1;
|
|
42
28
|
}
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
api.getEvents = async function (req, res) {
|
|
47
|
-
const startTs = toTs(req.query.start) || 0;
|
|
48
|
-
const endTs = toTs(req.query.end) || (Date.now() + 365 * 24 * 3600 * 1000);
|
|
49
|
-
|
|
50
|
-
const ids = await dbLayer.listReservationIdsByStartRange(startTs, endTs, 2000);
|
|
51
|
-
const out = [];
|
|
52
|
-
for (const rid of ids) {
|
|
53
|
-
const r = await dbLayer.getReservation(rid);
|
|
54
|
-
if (!r) continue;
|
|
55
|
-
if (r.status !== 'pending' && r.status !== 'approved') continue;
|
|
56
|
-
out.push(eventFor(r));
|
|
57
|
-
}
|
|
58
|
-
res.json(out);
|
|
59
|
-
};
|
|
30
|
+
let catalogCache = { at: 0, items: [], raw: null, ok: false, err: null };
|
|
60
31
|
|
|
61
|
-
|
|
32
|
+
async function getSettings() {
|
|
62
33
|
const settings = await meta.settings.get('calendar-onekite');
|
|
34
|
+
return settings || {};
|
|
35
|
+
}
|
|
63
36
|
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return
|
|
37
|
+
async function getCatalogItems() {
|
|
38
|
+
const settings = await getSettings();
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (catalogCache.ok && (now - catalogCache.at) < 5*60*1000) {
|
|
41
|
+
return catalogCache.items;
|
|
42
|
+
}
|
|
43
|
+
if (!settings.helloassoClientId || !settings.helloassoClientSecret || !settings.helloassoOrganizationSlug || !settings.helloassoFormSlug || !settings.helloassoFormType) {
|
|
44
|
+
catalogCache = { at: now, items: [], raw: null, ok: true, err: null };
|
|
45
|
+
return [];
|
|
73
46
|
}
|
|
47
|
+
try {
|
|
48
|
+
const pub = await hello.getShopCatalog(settings);
|
|
49
|
+
const items = hello.extractItemsFromPublic(pub);
|
|
50
|
+
catalogCache = { at: now, items, raw: pub, ok: true, err: null };
|
|
51
|
+
return items;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
catalogCache = { at: now, items: [], raw: null, ok: false, err: { message: err.message, statusCode: err.statusCode, body: err.body } };
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
74
57
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
formType: settings.helloassoFormType,
|
|
80
|
-
formSlug: settings.helloassoFormSlug,
|
|
81
|
-
});
|
|
58
|
+
async function getCatalog(req, res) {
|
|
59
|
+
const items = await getCatalogItems();
|
|
60
|
+
res.json({ ok: true, count: items.length, items });
|
|
61
|
+
}
|
|
82
62
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
63
|
+
async function getEvents(req, res) {
|
|
64
|
+
const start = parseDateParam(req.query.start);
|
|
65
|
+
const end = parseDateParam(req.query.end);
|
|
66
|
+
const startTs = start ? start.getTime() : Date.now() - 365*86400000;
|
|
67
|
+
const endTs = end ? end.getTime() : Date.now() + 365*86400000;
|
|
68
|
+
|
|
69
|
+
const ids = await dbi.listReservationIdsByStartRange(startTs, endTs, 5000);
|
|
70
|
+
const rows = await dbi.getReservations(ids);
|
|
71
|
+
|
|
72
|
+
const events = rows.map(r => {
|
|
73
|
+
const itemNames = (r.items || []).map(it => it.name).join(', ');
|
|
74
|
+
const title = itemNames || 'Réservation';
|
|
75
|
+
const status = r.status || 'pending';
|
|
76
|
+
const icon = status === 'approved' ? '✅' : status === 'refused' ? '⛔' : '⏳';
|
|
77
|
+
return {
|
|
78
|
+
id: r.id,
|
|
79
|
+
title: `${icon} ${title}`,
|
|
80
|
+
start: toYMD(r.startTs),
|
|
81
|
+
end: toYMD(r.endTs), // end exclusive works for allDay
|
|
82
|
+
allDay: true,
|
|
83
|
+
extendedProps: {
|
|
84
|
+
status,
|
|
85
|
+
requesterUid: r.uid,
|
|
86
|
+
items: r.items || [],
|
|
87
|
+
totalCents: r.totalCents || 0,
|
|
88
|
+
days: r.days || 1,
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
});
|
|
89
92
|
|
|
90
|
-
res.json(
|
|
91
|
-
}
|
|
93
|
+
res.json(events);
|
|
94
|
+
}
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
async function createReservation(req, res) {
|
|
94
97
|
const uid = req.uid;
|
|
95
|
-
if (!uid) return res.status(
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
if (!uid) return res.status(403).json({ status: { code: 'forbidden', message: 'Not logged in' } });
|
|
99
|
+
|
|
100
|
+
const body = req.body || {};
|
|
101
|
+
const startYMD = body.start;
|
|
102
|
+
const endYMD = body.end;
|
|
103
|
+
const itemIds = Array.isArray(body.itemIds) ? body.itemIds.map(String) : [];
|
|
104
|
+
if (!startYMD || !endYMD) return res.status(400).json({ status: { code: 'bad-request', message: 'Missing dates' } });
|
|
105
|
+
if (!itemIds.length) return res.status(400).json({ status: { code: 'bad-request', message: 'Missing itemIds' } });
|
|
106
|
+
|
|
107
|
+
const days = daysBetweenInclusive(startYMD, endYMD);
|
|
108
|
+
|
|
109
|
+
const catalog = await getCatalogItems();
|
|
110
|
+
const chosen = [];
|
|
111
|
+
let sumPerDay = 0;
|
|
112
|
+
for (const id of itemIds) {
|
|
113
|
+
const it = catalog.find(x => x.id === id);
|
|
114
|
+
if (it) {
|
|
115
|
+
chosen.push(it);
|
|
116
|
+
sumPerDay += Number(it.priceCents || 0);
|
|
117
|
+
} else {
|
|
118
|
+
// keep unknown item with 0 price to avoid hard fail
|
|
119
|
+
chosen.push({ id, name: `Item ${id}`, priceCents: 0 });
|
|
120
|
+
}
|
|
108
121
|
}
|
|
122
|
+
const totalCents = sumPerDay * days;
|
|
109
123
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
124
|
+
const id = await dbi.nextId();
|
|
125
|
+
const startTs = new Date(startYMD + 'T00:00:00Z').getTime();
|
|
126
|
+
const endTs = new Date(endYMD + 'T00:00:00Z').getTime();
|
|
112
127
|
|
|
113
128
|
const resv = {
|
|
114
|
-
|
|
115
|
-
uid,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
id,
|
|
130
|
+
uid: String(uid),
|
|
131
|
+
startTs,
|
|
132
|
+
endTs,
|
|
133
|
+
startYMD,
|
|
134
|
+
endYMD,
|
|
135
|
+
items: chosen,
|
|
136
|
+
days,
|
|
137
|
+
totalCents,
|
|
120
138
|
status: 'pending',
|
|
121
|
-
createdAt: now,
|
|
139
|
+
createdAt: Date.now(),
|
|
122
140
|
};
|
|
141
|
+
await dbi.saveReservation(resv);
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Notify groups by email
|
|
128
|
-
try {
|
|
129
|
-
const notifyGroups = (settings.notifyGroups || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
130
|
-
if (notifyGroups.length) {
|
|
131
|
-
const emailer = require.main.require('./src/emailer');
|
|
132
|
-
const u = await user.getUserData(uid);
|
|
133
|
-
for (const g of notifyGroups) {
|
|
134
|
-
const members = await groups.getMembers(g, 0, -1);
|
|
135
|
-
for (const m of members) {
|
|
136
|
-
const memberUid = typeof m === 'object' && m ? (m.uid || m.userId) : m;
|
|
137
|
-
const md = await user.getUserData(memberUid);
|
|
138
|
-
if (md && md.email) {
|
|
139
|
-
await emailer.send('calendar-onekite_pending', md.email, {
|
|
140
|
-
username: md.username,
|
|
141
|
-
requester: u.username,
|
|
142
|
-
itemName: resv.itemName,
|
|
143
|
-
start: new Date(start).toISOString(),
|
|
144
|
-
end: new Date(end).toISOString(),
|
|
145
|
-
rid,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} catch (e) {
|
|
152
|
-
// ignore email errors
|
|
153
|
-
}
|
|
143
|
+
res.json({ ok: true, reservation: resv });
|
|
144
|
+
}
|
|
154
145
|
|
|
155
|
-
|
|
146
|
+
module.exports = {
|
|
147
|
+
getEvents,
|
|
148
|
+
getCatalog,
|
|
149
|
+
createReservation,
|
|
150
|
+
_getCatalogItems: getCatalogItems,
|
|
151
|
+
_catalogCache: () => catalogCache,
|
|
156
152
|
};
|
|
157
|
-
|
|
158
|
-
module.exports = api;
|
package/lib/db.js
CHANGED
|
@@ -2,41 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
const db = require.main.require('./src/database');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const KEYS = {
|
|
6
|
+
Z_BY_START: 'calendar-onekite:reservations:byStart',
|
|
7
|
+
HASH: (id) => `calendar-onekite:reservation:${id}`,
|
|
8
|
+
NEXT_ID: 'calendar-onekite:reservation:nextId',
|
|
9
|
+
};
|
|
7
10
|
|
|
8
|
-
async function
|
|
9
|
-
|
|
11
|
+
async function nextId() {
|
|
12
|
+
const id = await db.incrObjectField('calendar-onekite:meta', 'nextId');
|
|
13
|
+
return String(id);
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
async function saveReservation(resv) {
|
|
13
|
-
await db.setObject(
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
await db.setObject(KEYS.HASH(resv.id), resv);
|
|
18
|
+
await db.sortedSetAdd(KEYS.Z_BY_START, resv.startTs, resv.id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getReservation(id) {
|
|
22
|
+
return await db.getObject(KEYS.HASH(id));
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
async function
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
async function getReservations(ids) {
|
|
26
|
+
if (!ids.length) return [];
|
|
27
|
+
const keys = ids.map(KEYS.HASH);
|
|
28
|
+
const objects = await db.getObjects(keys);
|
|
29
|
+
return objects.filter(Boolean);
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
async function listReservationIdsByStartRange(startTs, endTs, limit =
|
|
24
|
-
// NodeBB db
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const stop = Math.max(0, (parseInt(limit, 10) || 1000) - 1);
|
|
28
|
-
return await db.getSortedSetRangeByScore(KEY_ZSET, start, stop, startTs, endTs);
|
|
32
|
+
async function listReservationIdsByStartRange(startTs, endTs, limit = 2000) {
|
|
33
|
+
// NodeBB db API: getSortedSetRangeByScore(set, start, stop, min, max)
|
|
34
|
+
const ids = await db.getSortedSetRangeByScore(KEYS.Z_BY_START, 0, limit - 1, startTs, endTs);
|
|
35
|
+
return ids || [];
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
async function
|
|
32
|
-
|
|
38
|
+
async function deleteReservation(id) {
|
|
39
|
+
await db.delete(KEYS.HASH(id));
|
|
40
|
+
await db.sortedSetRemove(KEYS.Z_BY_START, id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function listAllIds(limit=100000) {
|
|
44
|
+
return await db.getSortedSetRange(KEYS.Z_BY_START, 0, limit-1);
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
module.exports = {
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
KEYS,
|
|
49
|
+
nextId,
|
|
38
50
|
saveReservation,
|
|
39
|
-
|
|
51
|
+
getReservation,
|
|
52
|
+
getReservations,
|
|
40
53
|
listReservationIdsByStartRange,
|
|
41
|
-
|
|
54
|
+
deleteReservation,
|
|
55
|
+
listAllIds,
|
|
42
56
|
};
|