nodebb-plugin-niki-loyalty 1.0.28 → 1.0.30

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.
Files changed (3) hide show
  1. package/library.js +193 -147
  2. package/package.json +1 -1
  3. package/static/lib/client.js +162 -104
package/library.js CHANGED
@@ -2,56 +2,54 @@
2
2
 
3
3
  const db = require.main.require('./src/database');
4
4
  const user = require.main.require('./src/user');
5
+ const posts = require.main.require('./src/posts');
5
6
  const routeHelpers = require.main.require('./src/controllers/helpers');
6
7
  const nconf = require.main.require('nconf');
7
8
 
8
9
  const Plugin = {};
9
10
 
10
11
  // =========================
11
- // SETTINGS & REWARDS
12
+ // ⚙️ AYARLAR & KURALLAR (GAME LOGIC)
12
13
  // =========================
13
14
  const SETTINGS = {
14
- pointsPerHeartbeat: 5,
15
- dailyCap: 250,
15
+ dailyCap: 28, // Günlük Maksimum Puan
16
16
  };
17
17
 
18
- // Rewards configuration (Ordered from highest cost to lowest)
18
+ // Puan Tablosu ve Limitleri
19
+ const ACTIONS = {
20
+ login: { points: 2, limit: 1, name: 'Günlük Giriş 👋' },
21
+ new_topic: { points: 7, limit: 1, name: 'Yeni Konu 📝' },
22
+ reply: { points: 3.5, limit: 2, name: 'Yorum Yazma 💬' },
23
+ read: { points: 1, limit: 8, name: 'Konu Okuma 👀' }, // Heartbeat ile çalışır
24
+ like_given: { points: 1, limit: 2, name: 'Beğeni Atma ❤️' },
25
+ like_taken: { points: 1, limit: 2, name: 'Beğeni Alma 🌟' }
26
+ };
27
+
28
+ // Ödüller
19
29
  const REWARDS = [
20
- { cost: 250, name: "Ücretsiz Kahve ☕" },
21
- { cost: 180, name: "%60 İndirimli Kahve" },
22
- { cost: 120, name: "%30 İndirimli Kahve" },
23
- { cost: 60, name: "1 Kurabiye 🍪" }
30
+ { cost: 250, name: 'Ücretsiz Kahve ☕' },
31
+ { cost: 180, name: '%60 İndirimli Kahve' },
32
+ { cost: 120, name: '%30 İndirimli Kahve' },
33
+ { cost: 60, name: '1 Kurabiye 🍪' },
24
34
  ];
25
35
 
26
- // TEST MODE (Set to true to bypass point checks)
27
36
  const TEST_MODE_UNLIMITED = false;
28
37
 
29
38
  // =========================
30
- // HELPER FUNCTIONS
39
+ // 🛠 YARDIMCI FONKSİYONLAR
31
40
  // =========================
32
41
  function safeParseMaybeJson(x) {
33
42
  if (x == null) return null;
34
43
  if (typeof x === 'object') return x;
35
- if (typeof x === 'string') {
36
- try { return JSON.parse(x); } catch (e) { return null; }
37
- }
38
- return null;
44
+ try { return JSON.parse(x); } catch (e) { return null; }
39
45
  }
40
46
 
41
47
  function safeStringify(obj) {
42
48
  try { return JSON.stringify(obj); } catch (e) { return null; }
43
49
  }
44
50
 
45
- // =========================
46
- // LOGGING
47
- // =========================
48
51
  async function addUserLog(uid, type, amount, desc) {
49
- const logEntry = {
50
- ts: Date.now(),
51
- type, // 'earn' | 'spend'
52
- amt: amount,
53
- txt: desc,
54
- };
52
+ const logEntry = { ts: Date.now(), type, amt: amount, txt: desc };
55
53
  const payload = safeStringify(logEntry);
56
54
  if (!payload) return;
57
55
  await db.listAppend(`niki:activity:${uid}`, payload);
@@ -59,13 +57,8 @@ async function addUserLog(uid, type, amount, desc) {
59
57
  }
60
58
 
61
59
  async function addKasaLog(staffUid, customerName, customerUid, rewardName, amount) {
62
- const logEntry = {
63
- ts: Date.now(),
64
- staff: staffUid,
65
- cust: customerName,
66
- cuid: customerUid,
67
- amt: amount,
68
- reward: rewardName // Store the specific reward name
60
+ const logEntry = {
61
+ ts: Date.now(), staff: staffUid, cust: customerName, cuid: customerUid, amt: amount, reward: rewardName
69
62
  };
70
63
  const payload = safeStringify(logEntry);
71
64
  if (!payload) return;
@@ -73,74 +66,152 @@ async function addKasaLog(staffUid, customerName, customerUid, rewardName, amoun
73
66
  await db.listTrim('niki:kasa:history', -100, -1);
74
67
  }
75
68
 
69
+ // 🔥 MERKEZİ PUAN DAĞITIM FONKSİYONU 🔥
70
+ // Bütün puan işlemleri buradan geçer, limitleri kontrol eder.
71
+ async function awardDailyAction(uid, actionKey) {
72
+ try {
73
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
74
+ const rule = ACTIONS[actionKey];
75
+
76
+ if (!rule) return;
77
+
78
+ // 1. Genel Günlük Limit Kontrolü (28 Puan)
79
+ const dailyScoreKey = `niki:daily:${uid}:${today}`;
80
+ const currentDailyScore = parseFloat((await db.getObjectField(dailyScoreKey, 'score')) || 0);
81
+
82
+ if (currentDailyScore >= SETTINGS.dailyCap) return; // Günlük limit doldu
83
+
84
+ // 2. Eylem Bazlı Limit Kontrolü (Örn: Max 2 Yorum)
85
+ const actionCountKey = `niki:daily:${uid}:${today}:counts`;
86
+ const currentActionCount = parseInt((await db.getObjectField(actionCountKey, actionKey)) || 0, 10);
87
+
88
+ if (currentActionCount >= rule.limit) return; // Bu eylemin hakkı doldu
89
+
90
+ // 3. Puan Ver ve Kaydet
91
+ let pointsToGive = rule.points;
92
+
93
+ // Eğer verilecek puan 28 sınırını aşıyorsa, sadece aradaki farkı ver
94
+ if (currentDailyScore + pointsToGive > SETTINGS.dailyCap) {
95
+ pointsToGive = SETTINGS.dailyCap - currentDailyScore;
96
+ }
97
+
98
+ if (pointsToGive <= 0) return;
99
+
100
+ // DB Güncellemeleri
101
+ await user.incrementUserFieldBy(uid, 'niki_points', pointsToGive); // Cüzdan
102
+ await db.incrObjectFieldBy(dailyScoreKey, 'score', pointsToGive); // Günlük Toplam
103
+ await db.incrObjectFieldBy(actionCountKey, actionKey, 1); // Eylem Sayacı
104
+
105
+ // Logla
106
+ await addUserLog(uid, 'earn', pointsToGive, rule.name);
107
+
108
+ } catch (err) {
109
+ console.error(`[Niki-Loyalty] Error awarding points for ${actionKey}:`, err);
110
+ }
111
+ }
112
+
113
+
114
+ // =========================
115
+ // ⚓ HOOKS (Olay Dinleyicileri)
116
+ // =========================
117
+
118
+ // 1. GÜNLÜK GİRİŞ (Login)
119
+ Plugin.onLogin = async function (data) {
120
+ if (!data || !data.uid) return;
121
+ await awardDailyAction(data.uid, 'login');
122
+ };
123
+
124
+ // 2. YENİ KONU AÇMA
125
+ Plugin.onTopicCreate = async function (data) {
126
+ // data.topic.uid konusuyu açan kişidir
127
+ if (!data || !data.topic || !data.topic.uid) return;
128
+ await awardDailyAction(data.topic.uid, 'new_topic');
129
+ };
130
+
131
+ // 3. YORUM YAZMA (Reply)
132
+ Plugin.onPostCreate = async function (data) {
133
+ if (!data || !data.post || !data.post.uid) return;
134
+
135
+ // Eğer post "main" ise (yani konunun kendisi ise) yorum sayılmaz, konu sayılır.
136
+ // NodeBB'de isMain kontrolü:
137
+ const isMain = await posts.isMain(data.post.pid);
138
+ if (isMain) return; // Bunu TopicCreate zaten yakalıyor
139
+
140
+ await awardDailyAction(data.post.uid, 'reply');
141
+ };
142
+
143
+ // 4. BEĞENİ (Like Atma ve Alma)
144
+ Plugin.onUpvote = async function (data) {
145
+ // data = { post: { pid, uid, ... }, uid: <like atan>, ... }
146
+ // Like Atan Kazanır:
147
+ if (data.uid) {
148
+ await awardDailyAction(data.uid, 'like_given');
149
+ }
150
+
151
+ // Like Alan Kazanır (Post sahibi):
152
+ if (data.post && data.post.uid && data.post.uid !== data.uid) {
153
+ // Kendine like atarsa kazanmasın
154
+ await awardDailyAction(data.post.uid, 'like_taken');
155
+ }
156
+ };
157
+
158
+
76
159
  // =========================
77
- // PLUGIN INIT
160
+ // 🚀 INIT & ROUTES
78
161
  // =========================
79
162
  Plugin.init = async function (params) {
80
163
  const router = params.router;
81
164
  const middleware = params.middleware;
82
165
 
83
- // 1) HEARTBEAT (Earn Points)
166
+ // 1) HEARTBEAT (Artık "Okuma" Puanı veriyor)
167
+ // Client-side script her 30-60 saniyede bir bu adrese istek atmalıdır.
84
168
  router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
85
169
  try {
86
170
  const uid = req.uid;
87
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
88
- const dailyKey = `niki:daily:${uid}:${today}`;
89
-
90
- const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
91
-
92
- if (currentDailyScore >= SETTINGS.dailyCap) {
93
- return res.json({ earned: false, reason: 'daily_cap' });
94
- }
95
-
96
- await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
97
- await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
98
-
171
+ // Heartbeat geldiğinde "read" aksiyonunu tetikle
172
+ await awardDailyAction(uid, 'read');
173
+
99
174
  const newBalance = await user.getUserField(uid, 'niki_points');
100
- return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
175
+ return res.json({ earned: true, total: newBalance });
101
176
  } catch (err) {
102
- return res.status(500).json({ earned: false, reason: 'server_error' });
177
+ return res.status(500).json({ error: 'error' });
103
178
  }
104
179
  });
105
180
 
106
- // 2) WALLET DATA (Data + History + Rewards Info)
181
+ // 2) WALLET DATA (Cüzdan Bilgileri)
107
182
  router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
108
183
  try {
109
184
  const uid = req.uid;
110
185
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
111
-
186
+
112
187
  const [userData, dailyData, historyRaw] = await Promise.all([
113
188
  user.getUserFields(uid, ['niki_points']),
114
189
  db.getObject(`niki:daily:${uid}:${today}`),
115
190
  db.getListRange(`niki:activity:${uid}`, 0, -1),
116
191
  ]);
117
192
 
118
- const currentPoints = parseInt(userData?.niki_points || 0, 10);
119
- const dailyScore = parseInt(dailyData?.score || 0, 10);
120
-
193
+ const dailyScore = parseFloat(dailyData?.score || 0);
121
194
  let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
122
195
  if (dailyPercent > 100) dailyPercent = 100;
123
196
 
124
- const history = (historyRaw || [])
125
- .map(safeParseMaybeJson)
126
- .filter(Boolean)
127
- .reverse();
197
+ const history = (historyRaw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
128
198
 
129
199
  return res.json({
130
- points: currentPoints,
200
+ points: parseInt(userData?.niki_points || 0, 10),
131
201
  dailyScore,
132
202
  dailyCap: SETTINGS.dailyCap,
133
203
  dailyPercent,
134
204
  history,
135
- rewards: REWARDS // Send reward tiers to frontend
205
+ rewards: REWARDS,
136
206
  });
137
207
  } catch (err) {
138
208
  return res.status(500).json({ points: 0, history: [] });
139
209
  }
140
210
  });
141
211
 
142
- // 3) KASA HISTORY
212
+ // 3) KASA HISTORY
143
213
  router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
214
+ // ... (Mevcut kodunun aynısı - sadece yetki kontrolü var)
144
215
  try {
145
216
  const isAdmin = await user.isAdministrator(req.uid);
146
217
  const isMod = await user.isGlobalModerator(req.uid);
@@ -148,116 +219,85 @@ Plugin.init = async function (params) {
148
219
 
149
220
  const raw = await db.getListRange('niki:kasa:history', 0, -1);
150
221
  const rows = (raw || []).map(safeParseMaybeJson).filter(Boolean).reverse();
151
-
222
+
223
+ // Kullanıcı detaylarını doldurma (Map logic)
152
224
  const uids = rows.map(r => parseInt(r.cuid, 10)).filter(n => Number.isFinite(n) && n > 0);
153
225
  const users = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'icon:bgColor']);
154
226
  const userMap = {};
155
- (users || []).forEach(u => { userMap[u.uid] = u; });
227
+ (users || []).forEach(u => userMap[u.uid] = u);
156
228
 
157
229
  const rp = nconf.get('relative_path') || '';
158
-
159
230
  const enriched = rows.map(r => {
160
- const uid = parseInt(r.cuid, 10);
161
- const u = userMap[uid];
162
- if (!u) return r;
231
+ const u = userMap[r.cuid] || {};
163
232
  return {
164
233
  ...r,
165
234
  cust: u.username || r.cust || 'Bilinmeyen',
166
- userslug: u.userslug || r.userslug || '',
167
- picture: u.picture || r.picture || '',
168
- profileUrl: (u.userslug ? `${rp}/user/${u.userslug}` : ''),
235
+ picture: u.picture || '',
236
+ iconBg: u['icon:bgColor'] || '#4b5563',
237
+ profileUrl: u.userslug ? `${rp}/user/${u.userslug}` : '',
238
+ reward: r.reward || 'İşlem'
169
239
  };
170
240
  });
171
-
172
241
  return res.json(enriched);
173
- } catch (err) {
174
- return res.status(500).json([]);
175
- }
242
+ } catch(e) { return res.status(500).json([]); }
176
243
  });
177
244
 
178
- // 4) GENERATE QR (Check if user has enough for MINIMUM reward)
245
+ // 4) QR OLUŞTURMA
179
246
  router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
180
247
  try {
181
- const uid = req.uid;
182
- const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
183
-
184
- // Get the cost of the cheapest reward
185
- const minCost = REWARDS[REWARDS.length - 1].cost;
186
-
187
- if (!TEST_MODE_UNLIMITED && points < minCost) {
188
- return res.json({ success: false, message: `En az ${minCost} puan gerekli.` });
189
- }
190
-
191
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
192
- await db.set(`niki:qr:${token}`, uid);
193
- await db.expire(`niki:qr:${token}`, 120);
194
-
195
- return res.json({ success: true, token });
196
- } catch (err) {
197
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
198
- }
248
+ const uid = req.uid;
249
+ const points = parseFloat((await user.getUserField(uid, 'niki_points')) || 0);
250
+ const minCost = REWARDS[REWARDS.length - 1].cost; // En ucuz ödül
251
+
252
+ if (!TEST_MODE_UNLIMITED && points < minCost) {
253
+ return res.json({ success: false, message: `Yetersiz Puan. En az ${minCost} gerekli.` });
254
+ }
255
+ const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
256
+ await db.set(`niki:qr:${token}`, uid);
257
+ await db.expire(`niki:qr:${token}`, 120); // 2 dakika geçerli
258
+ return res.json({ success: true, token });
259
+ } catch(e) { return res.status(500).json({ success: false }); }
199
260
  });
200
261
 
201
- // 5) SCAN QR (Determine Reward & Deduct Points)
262
+ // 5) QR TARATMA (Kasa İşlemi)
202
263
  router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
203
- try {
204
- const token = (req.body && req.body.token) ? String(req.body.token) : '';
205
- const isAdmin = await user.isAdministrator(req.uid);
206
- const isMod = await user.isGlobalModerator(req.uid);
207
- if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
208
-
209
- const customerUid = await db.get(`niki:qr:${token}`);
210
- if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
211
-
212
- const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
213
-
214
- // Calculate best possible reward
215
- let selectedReward = null;
216
- if (!TEST_MODE_UNLIMITED) {
217
- for (const reward of REWARDS) {
218
- if (pts >= reward.cost) {
219
- selectedReward = reward;
220
- break;
221
- }
222
- }
223
- if (!selectedReward) {
224
- return res.json({ success: false, message: 'Puan Yetersiz' });
225
- }
226
- } else {
227
- // Default for test mode
228
- selectedReward = REWARDS[0];
229
- }
230
-
231
- // Deduct Points
232
- if (!TEST_MODE_UNLIMITED) {
233
- await user.decrementUserFieldBy(customerUid, 'niki_points', selectedReward.cost);
234
- }
235
-
236
- await db.delete(`niki:qr:${token}`);
237
-
238
- const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
239
-
240
- // Logs
241
- await addUserLog(customerUid, 'spend', selectedReward.cost, selectedReward.name);
242
- await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, selectedReward.name, selectedReward.cost);
243
-
244
- return res.json({
245
- success: true,
246
- customer: customerData,
247
- rewardName: selectedReward.name,
248
- cost: selectedReward.cost,
249
- message: 'Onaylandı!',
250
- });
251
- } catch (err) {
252
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
253
- }
264
+ // ... (Mevcut kodunun aynısı)
265
+ try {
266
+ const token = req.body.token;
267
+ const isAdmin = await user.isAdministrator(req.uid);
268
+ const isMod = await user.isGlobalModerator(req.uid);
269
+ if (!isAdmin && !isMod) return res.status(403).json({success:false, message:'Yetkisiz'});
270
+
271
+ const custUid = await db.get(`niki:qr:${token}`);
272
+ if (!custUid) return res.json({success:false, message:'Geçersiz Kod'});
273
+
274
+ const pts = parseFloat(await user.getUserField(custUid, 'niki_points') || 0);
275
+
276
+ let selectedReward = null;
277
+ if (!TEST_MODE_UNLIMITED) {
278
+ for (const r of REWARDS) {
279
+ if (pts >= r.cost) { selectedReward = r; break; }
280
+ }
281
+ if (!selectedReward) return res.json({success:false, message:'Puan Yetersiz'});
282
+ } else { selectedReward = REWARDS[0]; }
283
+
284
+ if (!TEST_MODE_UNLIMITED) {
285
+ await user.decrementUserFieldBy(custUid, 'niki_points', selectedReward.cost);
286
+ }
287
+ await db.delete(`niki:qr:${token}`);
288
+
289
+ const cData = await user.getUserFields(custUid, ['username', 'picture', 'userslug']);
290
+ await addUserLog(custUid, 'spend', selectedReward.cost, selectedReward.name);
291
+ await addKasaLog(req.uid, cData.username, custUid, selectedReward.name, selectedReward.cost);
292
+
293
+ return res.json({ success: true, customer: cData, rewardName: selectedReward.name, cost: selectedReward.cost });
294
+ } catch(e) { return res.status(500).json({success:false}); }
254
295
  });
255
296
 
256
- // 6) PAGE ROUTES
297
+ // 6) SAYFA ROTALARI
257
298
  routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
258
- const isAdmin = await user.isAdministrator(req.uid);
259
- const isMod = await user.isGlobalModerator(req.uid);
260
- if (!isAdmin && !isMod) return res.render('403', {});
299
+ const isStaff = await user.isAdministrator(req.uid) || await user.isGlobalModerator(req.uid);
300
+ if (!isStaff) return res.render('403', {});
261
301
  return res.render('niki-kasa', { title: 'Niki Kasa' });
262
302
  });
263
303
  };
@@ -268,7 +308,13 @@ Plugin.addScripts = async function (scripts) {
268
308
  };
269
309
 
270
310
  Plugin.addNavigation = async function (nav) {
271
- nav.push({ route: '/niki-wallet', title: 'Niki Cüzdan', enabled: true, iconClass: 'fa-coffee', text: 'Niki Cüzdan' });
311
+ nav.push({
312
+ route: '/niki-wallet',
313
+ title: 'Niki Cüzdan',
314
+ enabled: true,
315
+ iconClass: 'fa-coffee',
316
+ text: 'Niki Cüzdan',
317
+ });
272
318
  return nav;
273
319
  };
274
320
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {
@@ -1,128 +1,186 @@
1
1
  'use strict';
2
2
 
3
+ /* globals $, app, socket, ajaxify, utils */
4
+
3
5
  $(document).ready(function () {
4
- // --- AYARLAR ---
5
- // 1. Logo Ayarı (Senin çalışan linkin)
6
- const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
6
+ let heartbeatInterval = null;
7
+
8
+ // Puanları güzel göstermek için yardımcı fonksiyon (Örn: 10.0 -> 10, 10.5 -> 10.5)
9
+ function formatPoints(points) {
10
+ let val = parseFloat(points);
11
+ if (isNaN(val)) return '0';
12
+ // Eğer tam sayı ise virgülsüz, değilse 1 basamaklı göster
13
+ return Number.isInteger(val) ? val.toFixed(0) : val.toFixed(1);
14
+ }
7
15
 
8
- // Widget HTML Şablonu
9
- const widgetHtml = `
10
- <div id="niki-floating-widget" class="niki-hidden">
11
- <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
12
- <img src="${NIKI_LOGO_URL}" class="niki-widget-logo" alt="Niki">
13
- <div class="niki-widget-text">
14
- <span class="niki-lbl">PUANIM</span>
15
- <span class="niki-val" id="niki-live-points">...</span>
16
- </div>
17
- </div>
18
- </div>
19
- `;
16
+ $(window).on('action:ajaxify.end', function (ev, data) {
17
+ // 1. ÖNCEKİ SAYAÇLARI TEMİZLE (Sayfa geçişlerinde üst üste binmesin)
18
+ if (heartbeatInterval) {
19
+ clearInterval(heartbeatInterval);
20
+ heartbeatInterval = null;
21
+ }
20
22
 
21
- // 1. Widget Başlatma ve Veri Yönetimi
22
- function initNikiWidget() {
23
- if (!app.user.uid || app.user.uid <= 0) return;
23
+ // ============================================================
24
+ // ❤️ KONU OKUMA (HEARTBEAT) SİSTEMİ
25
+ // ============================================================
26
+ // Sadece 'topic' (konu) sayfasındaysak sayaç çalışsın.
27
+ if (ajaxify.data.template.name === 'topic') {
28
+ console.log('[Niki-Loyalty] Konu sayfası algılandı, sayaç başlatılıyor...');
29
+
30
+ // 30 Saniyede bir tetikle (Günde 8 limit var backendde)
31
+ heartbeatInterval = setInterval(function () {
32
+ // Tarayıcı sekmesi aktif değilse gönderme (opsiyonel optimizasyon)
33
+ if (document.hidden) return;
24
34
 
25
- // Widget yoksa ekle
26
- if ($('#niki-floating-widget').length === 0) {
27
- $('body').append(widgetHtml);
35
+ $.post('/api/niki-loyalty/heartbeat', {}, function (response) {
36
+ if (response && response.earned) {
37
+ // Eğer puan kazandıysa ufak bir bildirim göster
38
+ app.alert({
39
+ type: 'success',
40
+ title: 'Puan Kazanıldı!',
41
+ message: 'Konu okuduğun için puan hesabına eklendi. 🐈',
42
+ timeout: 2500
43
+ });
44
+ console.log('[Niki-Loyalty] Heartbeat başarılı. Yeni Puan:', response.total);
45
+ }
46
+ });
47
+ }, 30000); // 30.000 ms = 30 Saniye
28
48
  }
29
49
 
30
- // --- HIZLI YÜKLEME (CACHE) ---
31
- // Önce hafızadaki son puanı hemen göster (Bekletme yapmaz)
32
- const cachedPoints = localStorage.getItem('niki_last_points');
33
- if (cachedPoints !== null) {
34
- $('#niki-live-points').text(cachedPoints);
35
- $('#niki-floating-widget').removeClass('niki-hidden');
50
+ // ============================================================
51
+ // 💰 CÜZDAN SAYFASI (niki-wallet)
52
+ // ============================================================
53
+ if (data.url === 'niki-wallet') {
54
+ loadWalletData();
36
55
  }
37
56
 
38
- // Logo Kontrolü (Garanti olsun)
39
- fixLogo();
57
+ // ============================================================
58
+ // 🏪 KASA SAYFASI (niki-kasa) - Yetkili İçin
59
+ // ============================================================
60
+ if (data.url === 'niki-kasa') {
61
+ loadKasaHistory(); // Geçmişi yükle
62
+ setupKasaScanner(); // QR okutma butonlarını ayarla
63
+ }
64
+ });
40
65
 
41
- // --- GÜNCEL VERİ ÇEKME ---
42
- // Arka planda sunucuya sor: "Puan değişti mi?"
43
- $.get('/api/niki-loyalty/wallet-data', function(data) {
44
- const freshPoints = data.points || 0;
45
-
46
- // Puanı güncelle
47
- $('#niki-live-points').text(freshPoints);
48
- $('#niki-floating-widget').removeClass('niki-hidden'); // İlk kez açılıyorsa göster
66
+ // -------------------------------------------------------------
67
+ // CÜZDAN FONKSİYONLARI
68
+ // -------------------------------------------------------------
69
+ function loadWalletData() {
70
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
71
+ // Puanları yerleştir (Decimal desteği ile)
72
+ $('#user-points').text(formatPoints(data.points));
73
+ $('#daily-score').text(formatPoints(data.dailyScore));
74
+ $('#daily-cap').text(data.dailyCap);
49
75
 
50
- // Yeni puanı hafızaya at (Bir sonraki giriş için)
51
- localStorage.setItem('niki_last_points', freshPoints);
76
+ // Progress Bar
77
+ const percent = data.dailyPercent > 100 ? 100 : data.dailyPercent;
78
+ $('#daily-progress').css('width', percent + '%').text(Math.round(percent) + '%');
79
+
80
+ // Geçmiş Tablosu
81
+ const historyList = $('#history-list');
82
+ historyList.empty();
52
83
 
53
- // Logoyu tekrar kontrol et (Resim geç yüklendiyse)
54
- fixLogo();
55
- }).fail(function() {
56
- // Hata olursa ve cache yoksa 0 yaz
57
- if (cachedPoints === null) {
58
- $('#niki-live-points').text('0');
59
- $('#niki-floating-widget').removeClass('niki-hidden');
84
+ if (data.history && data.history.length > 0) {
85
+ data.history.forEach(function (item) {
86
+ const colorClass = item.type === 'earn' ? 'text-success' : 'text-danger';
87
+ const sign = item.type === 'earn' ? '+' : '-';
88
+ const dateStr = new Date(item.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
89
+
90
+ const html = `
91
+ <li class="list-group-item d-flex justify-content-between align-items-center">
92
+ <div>
93
+ <small class="text-muted me-2">${dateStr}</small>
94
+ <span>${item.txt}</span>
95
+ </div>
96
+ <span class="fw-bold ${colorClass}">${sign}${formatPoints(item.amt)}</span>
97
+ </li>
98
+ `;
99
+ historyList.append(html);
100
+ });
101
+ } else {
102
+ historyList.append('<li class="list-group-item text-center text-muted">Henüz işlem yok.</li>');
60
103
  }
61
- });
62
- }
63
104
 
64
- // Logo Düzeltici (Senin çalışan kodun entegresi)
65
- function fixLogo() {
66
- const img = document.querySelector("img.niki-widget-logo");
67
- if (img && img.src !== NIKI_LOGO_URL) {
68
- img.src = NIKI_LOGO_URL;
69
- }
105
+ // QR Oluştur Butonu
106
+ $('#btn-generate-qr').off('click').on('click', function () {
107
+ $(this).prop('disabled', true);
108
+ $.post('/api/niki-loyalty/generate-qr', {}, function (res) {
109
+ $('#btn-generate-qr').prop('disabled', false);
110
+ if (res.success) {
111
+ // Basit bir modal veya alert ile kodu göster (veya QR kütüphanesi kullan)
112
+ // Şimdilik token'ı text olarak gösteriyoruz:
113
+ app.alert({
114
+ type: 'info',
115
+ title: 'Kod Oluşturuldu',
116
+ message: '<div class="text-center">Kasiyere bu kodu göster:<br><h2 style="margin:10px 0; letter-spacing:2px;">' + res.token + '</h2><small>2 dakika geçerli</small></div>',
117
+ timeout: 10000 // 10 saniye ekranda kalsın
118
+ });
119
+ } else {
120
+ app.alert({ type: 'danger', message: res.message });
121
+ }
122
+ });
123
+ });
124
+ });
70
125
  }
71
126
 
72
- // Başlat
73
- initNikiWidget();
74
-
75
- // Sayfa Geçişlerinde Tekrar Çalıştır
76
- $(window).on('action:ajaxify.end', function () {
77
- initNikiWidget();
78
- setTimeout(fixLogo, 500); // 0.5sn sonra son bir kontrol
79
- });
80
-
81
- // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
82
- let activeSeconds = 0;
83
- let isUserActive = false;
84
- let idleTimer;
85
-
86
- function resetIdleTimer() {
87
- isUserActive = true;
88
- clearTimeout(idleTimer);
89
- idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
90
- }
91
- $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
127
+ // -------------------------------------------------------------
128
+ // KASA FONKSİYONLARI (Admin/Mod)
129
+ // -------------------------------------------------------------
130
+ function loadKasaHistory() {
131
+ const tbody = $('#kasa-history-tbody');
132
+ if (tbody.length === 0) return;
92
133
 
93
- setInterval(() => {
94
- if (ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
95
- activeSeconds++;
96
- }
97
- if (activeSeconds >= 60) {
98
- sendHeartbeat();
99
- activeSeconds = 0;
100
- }
101
- }, 1000);
102
-
103
- function sendHeartbeat() {
104
- $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function(res) {
105
- if (res.earned) {
106
- // Puanı güncelle
107
- $('#niki-live-points').text(res.total);
108
- // Hafızayı da güncelle
109
- localStorage.setItem('niki_last_points', res.total);
110
-
111
- showNikiToast(`+${res.points} Puan Kazandın! ☕`);
112
- $('#niki-floating-widget').addClass('niki-bounce');
113
- setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
134
+ $.get('/api/niki-loyalty/kasa-history', function (rows) {
135
+ tbody.empty();
136
+ if (!rows || rows.length === 0) {
137
+ tbody.append('<tr><td colspan="5" class="text-center">Geçmiş işlem yok.</td></tr>');
138
+ return;
114
139
  }
140
+ rows.forEach(r => {
141
+ const dateStr = new Date(r.ts).toLocaleDateString() + ' ' + new Date(r.ts).toLocaleTimeString();
142
+ const rowHtml = `
143
+ <tr>
144
+ <td>${dateStr}</td>
145
+ <td>
146
+ <a href="${r.profileUrl}" target="_blank" class="text-decoration-none">
147
+ <span class="avatar avatar-sm" style="background-color: ${r.iconBg};">${r.cust.charAt(0).toUpperCase()}</span>
148
+ ${r.cust}
149
+ </a>
150
+ </td>
151
+ <td>${r.reward}</td>
152
+ <td class="text-danger">-${formatPoints(r.amt)}</td>
153
+ </tr>
154
+ `;
155
+ tbody.append(rowHtml);
156
+ });
115
157
  });
116
158
  }
117
159
 
118
- function showNikiToast(msg) {
119
- $('.niki-toast').remove();
120
- const toast = $(`<div class="niki-toast"><i class="fa fa-paw"></i> ${msg}</div>`);
121
- $('body').append(toast);
122
- setTimeout(() => { toast.addClass('show'); }, 100);
123
- setTimeout(() => {
124
- toast.removeClass('show');
125
- setTimeout(() => toast.remove(), 3000);
126
- }, 3000);
160
+ function setupKasaScanner() {
161
+ $('#form-scan-qr').off('submit').on('submit', function (e) {
162
+ e.preventDefault();
163
+ const token = $('#qr-input').val().trim();
164
+ if (!token) return;
165
+
166
+ $.post('/api/niki-loyalty/scan-qr', { token: token }, function (res) {
167
+ if (res.success) {
168
+ app.alert({
169
+ type: 'success',
170
+ title: 'İşlem Başarılı! ✅',
171
+ message: `
172
+ <strong>Müşteri:</strong> ${res.customer.username}<br>
173
+ <strong>Verilen:</strong> ${res.rewardName}<br>
174
+ <strong>Tutar:</strong> ${res.cost} Puan
175
+ `,
176
+ timeout: 5000
177
+ });
178
+ $('#qr-input').val(''); // Inputu temizle
179
+ loadKasaHistory(); // Tabloyu güncelle
180
+ } else {
181
+ app.alert({ type: 'danger', title: 'Hata', message: res.message });
182
+ }
183
+ });
184
+ });
127
185
  }
128
186
  });