nodebb-plugin-niki-loyalty 1.2.9 → 1.3.0
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 +270 -39
- package/package.json +1 -1
- package/static/widgets/niki-admin.tpl +686 -0
- package/templates/niki-kasa.tpl +562 -0
package/library.js
CHANGED
|
@@ -169,51 +169,69 @@ Plugin.onPostCreate = async function (data) {
|
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
// 4. BEĞENİ (Like Atma ve Alma) - Spam Korumalı + Debug Loglı
|
|
172
|
+
// NodeBB upvote hook'u { pid, uid, ... } formatında data gönderir (post nesnesi değil!)
|
|
172
173
|
Plugin.onUpvote = async function (data) {
|
|
173
|
-
console.log('[Niki-Loyalty] 👍 Upvote hook tetiklendi. Data:', JSON.stringify(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
console.log('[Niki-Loyalty] 👍 Upvote hook tetiklendi. Raw Data:', JSON.stringify(data));
|
|
175
|
+
|
|
176
|
+
// NodeBB bazen farklı formatlar gönderebilir, hepsini kontrol et
|
|
177
|
+
const pid = data.pid || (data.post && data.post.pid);
|
|
178
|
+
const voterUid = data.uid || (data.current && data.current.uid);
|
|
178
179
|
|
|
179
|
-
const pid = data.post && data.post.pid;
|
|
180
180
|
if (!pid) {
|
|
181
181
|
console.log('[Niki-Loyalty] ⚠️ Post PID bulunamadı, işlem iptal.');
|
|
182
182
|
return;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
if (!voterUid) {
|
|
186
|
+
console.log('[Niki-Loyalty] ⚠️ Voter UID bulunamadı, işlem iptal.');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Post sahibini bul (NodeBB upvote hook'u post sahibini göndermez!)
|
|
191
|
+
let postOwnerUid;
|
|
192
|
+
try {
|
|
193
|
+
postOwnerUid = await posts.getPostField(pid, 'uid');
|
|
194
|
+
console.log(`[Niki-Loyalty] Post sahibi bulundu: PID=${pid}, Owner UID=${postOwnerUid}`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.log('[Niki-Loyalty] ⚠️ Post sahibi bulunamadı:', err.message);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!postOwnerUid) {
|
|
201
|
+
console.log('[Niki-Loyalty] ⚠️ Post sahibi UID boş, işlem iptal.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
185
205
|
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
186
206
|
|
|
187
207
|
// Like Atan Kazanır:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const alreadyLiked = await db.isSetMember(likeGivenKey, pid.toString());
|
|
208
|
+
const likeGivenKey = `niki:liked:${voterUid}:${today}`;
|
|
209
|
+
const alreadyLiked = await db.isSetMember(likeGivenKey, pid.toString());
|
|
191
210
|
|
|
192
|
-
|
|
211
|
+
console.log(`[Niki-Loyalty] Like Atan: UID=${voterUid}, PID=${pid}, Daha önce beğenmiş mi=${alreadyLiked}`);
|
|
193
212
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
213
|
+
if (!alreadyLiked) {
|
|
214
|
+
const result = await awardDailyAction(voterUid, 'like_given');
|
|
215
|
+
console.log('[Niki-Loyalty] like_given sonuç:', result);
|
|
216
|
+
await db.setAdd(likeGivenKey, pid.toString());
|
|
217
|
+
await db.expire(likeGivenKey, 86400);
|
|
200
218
|
}
|
|
201
219
|
|
|
202
|
-
// Like Alan Kazanır (Post sahibi):
|
|
203
|
-
if (
|
|
204
|
-
const likeTakenKey = `niki:liked_taken:${
|
|
220
|
+
// Like Alan Kazanır (Post sahibi - kendine beğeni atamaz):
|
|
221
|
+
if (postOwnerUid && postOwnerUid !== voterUid) {
|
|
222
|
+
const likeTakenKey = `niki:liked_taken:${postOwnerUid}:${today}`;
|
|
205
223
|
const alreadyTaken = await db.isSetMember(likeTakenKey, pid.toString());
|
|
206
224
|
|
|
207
|
-
console.log(`[Niki-Loyalty] Like Alan: UID=${
|
|
225
|
+
console.log(`[Niki-Loyalty] Like Alan: UID=${postOwnerUid}, PID=${pid}, Daha önce puan almış mı=${alreadyTaken}`);
|
|
208
226
|
|
|
209
227
|
if (!alreadyTaken) {
|
|
210
|
-
const result = await awardDailyAction(
|
|
228
|
+
const result = await awardDailyAction(postOwnerUid, 'like_taken');
|
|
211
229
|
console.log('[Niki-Loyalty] like_taken sonuç:', result);
|
|
212
230
|
await db.setAdd(likeTakenKey, pid.toString());
|
|
213
231
|
await db.expire(likeTakenKey, 86400);
|
|
214
232
|
}
|
|
215
233
|
} else {
|
|
216
|
-
console.log('[Niki-Loyalty] ⚠️
|
|
234
|
+
console.log('[Niki-Loyalty] ⚠️ Kullanıcı kendi postunu beğenmiş veya post sahibi bulunamadı. Post owner:', postOwnerUid, 'Voter:', voterUid);
|
|
217
235
|
}
|
|
218
236
|
};
|
|
219
237
|
|
|
@@ -301,37 +319,113 @@ Plugin.init = async function (params) {
|
|
|
301
319
|
}
|
|
302
320
|
});
|
|
303
321
|
|
|
304
|
-
// 3) KASA HISTORY
|
|
322
|
+
// 3) KASA HISTORY - GELİŞMİŞ VERSİYON (Filtre + İstatistik)
|
|
305
323
|
router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
|
|
306
|
-
// ... (Mevcut kodunun aynısı - sadece yetki kontrolü var)
|
|
307
324
|
try {
|
|
308
325
|
const isAdmin = await user.isAdministrator(req.uid);
|
|
309
326
|
const isMod = await user.isGlobalModerator(req.uid);
|
|
310
|
-
if (!isAdmin && !isMod) return res.status(403).json(
|
|
327
|
+
if (!isAdmin && !isMod) return res.status(403).json({ error: 'Yetkisiz' });
|
|
328
|
+
|
|
329
|
+
// Query parametreleri
|
|
330
|
+
const { startDate, endDate, search, rewardType, exportAll } = req.query;
|
|
311
331
|
|
|
312
332
|
const raw = await db.getListRange('niki:kasa:history', 0, -1);
|
|
313
|
-
|
|
333
|
+
let rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
|
|
314
334
|
|
|
315
|
-
//
|
|
316
|
-
const
|
|
317
|
-
const
|
|
335
|
+
// Personel bilgilerini de al
|
|
336
|
+
const staffUids = [...new Set(rows.map(r => parseInt(r.staff, 10)).filter(n => Number.isFinite(n) && n > 0))];
|
|
337
|
+
const custUids = rows.map(r => parseInt(r.cuid, 10)).filter(n => Number.isFinite(n) && n > 0);
|
|
338
|
+
const allUids = [...new Set([...staffUids, ...custUids])];
|
|
339
|
+
|
|
340
|
+
const usersData = await user.getUsersFields(allUids, ['uid', 'username', 'userslug', 'picture', 'icon:bgColor']);
|
|
318
341
|
const userMap = {};
|
|
319
|
-
(
|
|
342
|
+
(usersData || []).forEach(u => userMap[u.uid] = u);
|
|
320
343
|
|
|
321
344
|
const rp = nconf.get('relative_path') || '';
|
|
322
|
-
|
|
323
|
-
|
|
345
|
+
|
|
346
|
+
// Zenginleştir
|
|
347
|
+
let enriched = rows.map(r => {
|
|
348
|
+
const custUser = userMap[r.cuid] || {};
|
|
349
|
+
const staffUser = userMap[r.staff] || {};
|
|
324
350
|
return {
|
|
325
351
|
...r,
|
|
326
|
-
cust:
|
|
327
|
-
picture:
|
|
328
|
-
iconBg:
|
|
329
|
-
profileUrl:
|
|
330
|
-
reward: r.reward || 'İşlem'
|
|
352
|
+
cust: custUser.username || r.cust || 'Bilinmeyen',
|
|
353
|
+
picture: custUser.picture || '',
|
|
354
|
+
iconBg: custUser['icon:bgColor'] || '#4b5563',
|
|
355
|
+
profileUrl: custUser.userslug ? `${rp}/user/${custUser.userslug}` : '',
|
|
356
|
+
reward: r.reward || 'İşlem',
|
|
357
|
+
staffName: staffUser.username || 'Personel',
|
|
358
|
+
staffPicture: staffUser.picture || '',
|
|
359
|
+
date: new Date(r.ts).toISOString().slice(0, 10) // YYYY-MM-DD
|
|
331
360
|
};
|
|
332
361
|
});
|
|
333
|
-
|
|
334
|
-
|
|
362
|
+
|
|
363
|
+
// FİLTRELEME
|
|
364
|
+
// 1. Tarih aralığı
|
|
365
|
+
if (startDate) {
|
|
366
|
+
const start = new Date(startDate).getTime();
|
|
367
|
+
enriched = enriched.filter(r => r.ts >= start);
|
|
368
|
+
}
|
|
369
|
+
if (endDate) {
|
|
370
|
+
const end = new Date(endDate).getTime() + 86400000; // gün sonu
|
|
371
|
+
enriched = enriched.filter(r => r.ts < end);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 2. Arama (kullanıcı adı)
|
|
375
|
+
if (search && search.trim()) {
|
|
376
|
+
const q = search.toLowerCase().trim();
|
|
377
|
+
enriched = enriched.filter(r =>
|
|
378
|
+
(r.cust && r.cust.toLowerCase().includes(q)) ||
|
|
379
|
+
(r.staffName && r.staffName.toLowerCase().includes(q))
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 3. Ödül tipi
|
|
384
|
+
if (rewardType && rewardType !== 'all') {
|
|
385
|
+
enriched = enriched.filter(r => r.reward === rewardType);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// İSTATİSTİKLER
|
|
389
|
+
const stats = {
|
|
390
|
+
totalTransactions: enriched.length,
|
|
391
|
+
totalPoints: enriched.reduce((sum, r) => sum + (parseFloat(r.amt) || 0), 0),
|
|
392
|
+
byReward: {},
|
|
393
|
+
byStaff: {},
|
|
394
|
+
byDate: {}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
enriched.forEach(r => {
|
|
398
|
+
// Ödül bazında
|
|
399
|
+
stats.byReward[r.reward] = (stats.byReward[r.reward] || 0) + 1;
|
|
400
|
+
// Personel bazında
|
|
401
|
+
stats.byStaff[r.staffName] = (stats.byStaff[r.staffName] || 0) + 1;
|
|
402
|
+
// Gün bazında (son 7 gün için chart)
|
|
403
|
+
stats.byDate[r.date] = (stats.byDate[r.date] || 0) + 1;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Benzersiz ödül tipleri (filter dropdown için)
|
|
407
|
+
const rewardTypes = [...new Set(rows.map(r => r.reward || 'İşlem'))];
|
|
408
|
+
|
|
409
|
+
// Export all için sayfalama yok
|
|
410
|
+
if (exportAll === 'true') {
|
|
411
|
+
return res.json({
|
|
412
|
+
data: enriched,
|
|
413
|
+
stats,
|
|
414
|
+
rewardTypes
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Normal görünüm (son 100 işlem)
|
|
419
|
+
return res.json({
|
|
420
|
+
data: enriched.slice(0, 100),
|
|
421
|
+
stats,
|
|
422
|
+
rewardTypes,
|
|
423
|
+
hasMore: enriched.length > 100
|
|
424
|
+
});
|
|
425
|
+
} catch (e) {
|
|
426
|
+
console.error('[Niki-Loyalty] Kasa history error:', e);
|
|
427
|
+
return res.status(500).json({ error: 'Sunucu hatası' });
|
|
428
|
+
}
|
|
335
429
|
});
|
|
336
430
|
|
|
337
431
|
// 4) QR OLUŞTURMA
|
|
@@ -566,10 +660,147 @@ Plugin.adminManagePoints = async function (socket, data) {
|
|
|
566
660
|
const newPoints = await user.getUserField(targetUid, 'niki_points');
|
|
567
661
|
return { success: true, newPoints: parseFloat(newPoints) };
|
|
568
662
|
};
|
|
663
|
+
// 4) KULLANICI DETAY (Admin için)
|
|
664
|
+
Plugin.adminGetUserDetail = async function (socket, data) {
|
|
665
|
+
const uid = socket.uid;
|
|
666
|
+
if (!uid) throw new Error('Giriş yapmalısınız.');
|
|
667
|
+
|
|
668
|
+
const isAdmin = await user.isAdministrator(uid);
|
|
669
|
+
const isMod = await user.isGlobalModerator(uid);
|
|
670
|
+
if (!isAdmin && !isMod) throw new Error('Yetkisiz Erişim');
|
|
671
|
+
|
|
672
|
+
const targetUid = data.uid;
|
|
673
|
+
if (!targetUid) throw new Error('Kullanıcı ID gerekli.');
|
|
674
|
+
|
|
675
|
+
// Kullanıcı bilgileri
|
|
676
|
+
const userData = await user.getUserFields(targetUid, [
|
|
677
|
+
'uid', 'username', 'userslug', 'picture', 'email',
|
|
678
|
+
'niki_points', 'icon:bgColor', 'joindate', 'lastonline'
|
|
679
|
+
]);
|
|
680
|
+
|
|
681
|
+
if (!userData || !userData.uid) throw new Error('Kullanıcı bulunamadı.');
|
|
682
|
+
|
|
683
|
+
// Aktivite geçmişi
|
|
684
|
+
const activityRaw = await db.getListRange(`niki:activity:${targetUid}`, 0, -1);
|
|
685
|
+
const activities = (activityRaw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
|
|
686
|
+
|
|
687
|
+
// Kazanılan ve harcanan puanları ayır
|
|
688
|
+
let totalEarned = 0;
|
|
689
|
+
let totalSpent = 0;
|
|
690
|
+
const earnHistory = [];
|
|
691
|
+
const spendHistory = [];
|
|
692
|
+
|
|
693
|
+
activities.forEach(a => {
|
|
694
|
+
if (a.type === 'earn' || a.type === 'admin_adjust') {
|
|
695
|
+
if (a.type === 'admin_adjust' && a.txt && a.txt.includes('-')) {
|
|
696
|
+
totalSpent += parseFloat(a.amt) || 0;
|
|
697
|
+
spendHistory.push(a);
|
|
698
|
+
} else {
|
|
699
|
+
totalEarned += parseFloat(a.amt) || 0;
|
|
700
|
+
earnHistory.push(a);
|
|
701
|
+
}
|
|
702
|
+
} else if (a.type === 'spend') {
|
|
703
|
+
totalSpent += parseFloat(a.amt) || 0;
|
|
704
|
+
spendHistory.push(a);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Günlük limit durumu
|
|
709
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
710
|
+
const dailyData = await db.getObject(`niki:daily:${targetUid}:${today}`);
|
|
711
|
+
const actionCounts = await db.getObject(`niki:daily:${targetUid}:${today}:counts`);
|
|
712
|
+
|
|
713
|
+
// Bu kullanıcının kasa işlemleri (harcamaları)
|
|
714
|
+
const kasaRaw = await db.getListRange('niki:kasa:history', 0, -1);
|
|
715
|
+
const userKasaHistory = (kasaRaw || [])
|
|
716
|
+
.map(safeParseMaybeJson)
|
|
717
|
+
.filter(k => k && String(k.cuid) === String(targetUid))
|
|
718
|
+
.reverse()
|
|
719
|
+
.slice(0, 20);
|
|
720
|
+
|
|
721
|
+
const rp = require.main.require('nconf').get('relative_path') || '';
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
user: {
|
|
725
|
+
uid: userData.uid,
|
|
726
|
+
username: userData.username,
|
|
727
|
+
userslug: userData.userslug,
|
|
728
|
+
picture: userData.picture || '',
|
|
729
|
+
email: userData.email || '',
|
|
730
|
+
iconBg: userData['icon:bgColor'] || '#4b5563',
|
|
731
|
+
points: parseFloat(userData.niki_points || 0),
|
|
732
|
+
joindate: userData.joindate,
|
|
733
|
+
lastonline: userData.lastonline,
|
|
734
|
+
profileUrl: userData.userslug ? `${rp}/user/${userData.userslug}` : ''
|
|
735
|
+
},
|
|
736
|
+
stats: {
|
|
737
|
+
totalEarned,
|
|
738
|
+
totalSpent,
|
|
739
|
+
currentPoints: parseFloat(userData.niki_points || 0),
|
|
740
|
+
todayScore: parseFloat(dailyData?.score || 0),
|
|
741
|
+
todayCounts: actionCounts || {}
|
|
742
|
+
},
|
|
743
|
+
earnHistory: earnHistory.slice(0, 30),
|
|
744
|
+
spendHistory: spendHistory.slice(0, 30),
|
|
745
|
+
kasaHistory: userKasaHistory,
|
|
746
|
+
actions: ACTIONS
|
|
747
|
+
};
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// 5) GENEL İSTATİSTİKLER (Dashboard için)
|
|
751
|
+
Plugin.adminGetStats = async function (socket, data) {
|
|
752
|
+
const uid = socket.uid;
|
|
753
|
+
if (!uid) throw new Error('Giriş yapmalısınız.');
|
|
754
|
+
|
|
755
|
+
const isAdmin = await user.isAdministrator(uid);
|
|
756
|
+
const isMod = await user.isGlobalModerator(uid);
|
|
757
|
+
if (!isAdmin && !isMod) throw new Error('Yetkisiz Erişim');
|
|
758
|
+
|
|
759
|
+
// Tüm kullanıcıları al
|
|
760
|
+
const uids = await db.getSortedSetRange('users:joindate', 0, 499);
|
|
761
|
+
if (!uids || uids.length === 0) return { users: 0, totalPoints: 0, avgPoints: 0 };
|
|
762
|
+
|
|
763
|
+
const usersData = await user.getUsersFields(uids, ['niki_points']);
|
|
764
|
+
|
|
765
|
+
let totalPoints = 0;
|
|
766
|
+
let usersWithPoints = 0;
|
|
767
|
+
|
|
768
|
+
usersData.forEach(u => {
|
|
769
|
+
const pts = parseFloat(u.niki_points || 0);
|
|
770
|
+
if (pts > 0) {
|
|
771
|
+
totalPoints += pts;
|
|
772
|
+
usersWithPoints++;
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Kasa geçmişinden toplam harcama
|
|
777
|
+
const kasaRaw = await db.getListRange('niki:kasa:history', 0, -1);
|
|
778
|
+
const kasaData = (kasaRaw || []).map(safeParseMaybeJson).filter(Boolean);
|
|
779
|
+
const totalRedeemed = kasaData.reduce((sum, k) => sum + (parseFloat(k.amt) || 0), 0);
|
|
780
|
+
|
|
781
|
+
// Bugünkü işlemler
|
|
782
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
783
|
+
const todayTransactions = kasaData.filter(k => {
|
|
784
|
+
const d = new Date(k.ts).toISOString().slice(0, 10);
|
|
785
|
+
return d === today;
|
|
786
|
+
}).length;
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
usersWithPoints,
|
|
790
|
+
totalPoints: Math.floor(totalPoints),
|
|
791
|
+
avgPoints: usersWithPoints > 0 ? Math.floor(totalPoints / usersWithPoints) : 0,
|
|
792
|
+
totalRedeemed: Math.floor(totalRedeemed),
|
|
793
|
+
totalTransactions: kasaData.length,
|
|
794
|
+
todayTransactions
|
|
795
|
+
};
|
|
796
|
+
};
|
|
797
|
+
|
|
569
798
|
// Soket'e kaydet (Client: socket.emit('plugins.niki.getUsers', ...))
|
|
570
799
|
if (SocketPlugins) {
|
|
571
800
|
SocketPlugins.niki = {
|
|
572
801
|
getUsers: Plugin.adminGetUsers,
|
|
802
|
+
getUserDetail: Plugin.adminGetUserDetail,
|
|
803
|
+
getStats: Plugin.adminGetStats,
|
|
573
804
|
scanQR: Plugin.socketScanQR,
|
|
574
805
|
getKasaHistory: Plugin.socketKasaHistory,
|
|
575
806
|
managePoints: Plugin.adminManagePoints
|