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 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
- voterUid: data.uid,
175
- postPid: data.post?.pid,
176
- postOwnerUid: data.post?.uid
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
- if (data.uid) {
189
- const likeGivenKey = `niki:liked:${data.uid}:${today}`;
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
- console.log(`[Niki-Loyalty] Like Atan: UID=${data.uid}, PID=${pid}, Daha önce beğenmiş mi=${alreadyLiked}`);
211
+ console.log(`[Niki-Loyalty] Like Atan: UID=${voterUid}, PID=${pid}, Daha önce beğenmiş mi=${alreadyLiked}`);
193
212
 
194
- if (!alreadyLiked) {
195
- const result = await awardDailyAction(data.uid, 'like_given');
196
- console.log('[Niki-Loyalty] like_given sonuç:', result);
197
- await db.setAdd(likeGivenKey, pid.toString());
198
- await db.expire(likeGivenKey, 86400);
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 (data.post && data.post.uid && data.post.uid !== data.uid) {
204
- const likeTakenKey = `niki:liked_taken:${data.post.uid}:${today}`;
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=${data.post.uid}, PID=${pid}, Daha önce puan almış mı=${alreadyTaken}`);
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(data.post.uid, 'like_taken');
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] ⚠️ Like alan kontrol edilemedi. Post owner:', data.post?.uid, 'Voter:', data.uid);
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
- const rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
333
+ let rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
314
334
 
315
- // Kullanıcı detaylarını doldurma (Map logic)
316
- const uids = rows.map(r => parseInt(r.cuid, 10)).filter(n => Number.isFinite(n) && n > 0);
317
- const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'icon:bgColor']);
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
- (users || []).forEach(u => userMap[u.uid] = u);
342
+ (usersData || []).forEach(u => userMap[u.uid] = u);
320
343
 
321
344
  const rp = nconf.get('relative_path') || '';
322
- const enriched = rows.map(r => {
323
- const u = userMap[r.cuid] || {};
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: u.username || r.cust || 'Bilinmeyen',
327
- picture: u.picture || '',
328
- iconBg: u['icon:bgColor'] || '#4b5563',
329
- profileUrl: u.userslug ? `${rp}/user/${u.userslug}` : '',
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
- return res.json(enriched);
334
- } catch (e) { return res.status(500).json([]); }
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.2.9",
3
+ "version": "1.3.0",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {