nodebb-plugin-niki-loyalty 1.3.0 → 1.3.1

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.
@@ -0,0 +1,506 @@
1
+ <script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
2
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
3
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/locale/tr.min.js"></script>
4
+
5
+ <style>
6
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap');
7
+ body.niki-kasa-body { background-color: #000; margin: 0; padding: 0; overflow: hidden; }
8
+ #content { padding: 0 !important; margin: 0 !important; width: 100% !important; background: #000; }
9
+
10
+ /* GİRİŞ */
11
+ .start-view { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #111; z-index: 100; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Poppins', sans-serif; }
12
+ .logo-box { width: 90px; height: 90px; border-radius: 50%; border: 3px solid #C5A065; display: flex; align-items: center; justify-content: center; margin-bottom: 30px; box-shadow: 0 0 30px rgba(197, 160, 101, 0.2); background: #000; }
13
+ .btn-start { background: #C5A065; color: #000; padding: 18px 40px; border-radius: 14px; font-weight: 700; border: none; cursor: pointer; display: flex; align-items: center; gap: 10px; font-size: 16px; margin-bottom: 15px; width: 260px; justify-content: center; box-shadow: 0 5px 15px rgba(197, 160, 101, 0.3); }
14
+ .btn-hist { background: #222; color: #fff; padding: 18px 40px; border-radius: 14px; font-weight: 600; border: 1px solid #333; cursor: pointer; display: flex; align-items: center; gap: 10px; font-size: 14px; width: 260px; justify-content: center; }
15
+
16
+ /* KAMERA */
17
+ .camera-ui { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; display: none; z-index: 200; }
18
+ #reader { width: 100% !important; height: 100% !important; border: none !important; }
19
+ #reader video { object-fit: cover !important; width: 100% !important; height: 100% !important; }
20
+
21
+ .session-bar-bg { position: absolute; top:0; left:0; width:100%; height:6px; background:rgba(255,255,255,0.2); z-index: 220; }
22
+ .session-bar-fill { height:100%; background:#C5A065; width:100%; transition: width 1s linear; }
23
+
24
+ .top-ctrl { position: absolute; top: 20px; left: 0; width: 100%; padding: 0 20px; display: flex; justify-content: space-between; z-index: 230; box-sizing: border-box; }
25
+ .badge-live { background: rgba(0,0,0,0.6); color: #C5A065; padding: 6px 14px; border-radius: 20px; font-size: 12px; font-weight: 700; border: 1px solid rgba(197, 160, 101, 0.4); backdrop-filter: blur(4px); }
26
+ .btn-x { background: rgba(255,255,255,0.2); width: 40px; height: 40px; border-radius: 50%; border: none; color: #fff; cursor: pointer; font-size: 18px; backdrop-filter: blur(4px); display:flex; align-items:center; justify-content:center; }
27
+
28
+ /* VİZÖR */
29
+ .scan-guide { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 70vw; height: 70vw; max-width: 300px; max-height: 300px; box-shadow: 0 0 0 4000px rgba(0,0,0,0.85); border: 2px solid rgba(255,255,255,0.3); border-radius: 24px; pointer-events: none; z-index: 210; }
30
+ .scan-guide::before { content:''; position:absolute; top:-2px; left:-2px; width:40px; height:40px; border-top:5px solid #C5A065; border-left:5px solid #C5A065; border-radius:20px 0 0 0; }
31
+ .scan-guide::after { content:''; position:absolute; bottom:-2px; right:-2px; width:40px; height:40px; border-bottom:5px solid #C5A065; border-right:5px solid #C5A065; border-radius:0 0 20px 0; }
32
+ .laser { width: 100%; height: 2px; background: #C5A065; position: absolute; top: 50%; box-shadow: 0 0 15px #C5A065; animation: scanAnim 2s infinite ease-in-out; }
33
+ @keyframes scanAnim { 0% { top: 10%; opacity: 0; } 50% { opacity: 1; } 100% { top: 90%; opacity: 0; } }
34
+
35
+ .scan-hint { position: absolute; bottom: 60px; width: 100%; text-align: center; color: #fff; font-size: 13px; opacity: 0.8; z-index: 220; letter-spacing: 0.5px; }
36
+
37
+ /* SONUÇ */
38
+ .res-modal { position: fixed; bottom: 0; left: 0; width: 100%; background: #1a1a1a; border-radius: 30px 30px 0 0; padding: 40px 20px; text-align: center; transform: translateY(110%); transition: transform 0.3s; z-index: 600; box-sizing: border-box; border-top: 1px solid #333; }
39
+ .res-modal.active { transform: translateY(0); }
40
+ .icon-big { font-size: 60px; margin-bottom: 15px; }
41
+ .res-title { color: #fff; font-size: 24px; font-weight: 700; margin-bottom: 10px; }
42
+ .res-msg { color: #aaa; font-size: 15px; margin-bottom: 30px; }
43
+ .btn-new { background: #fff; color: #000; width: 100%; padding: 18px; border-radius: 16px; font-size: 16px; font-weight: 700; border: none; cursor: pointer; }
44
+
45
+ /* GEÇMİŞ - GELİŞMİŞ */
46
+ .hist-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #0a0a0a; z-index: 700; display: none; flex-direction: column; font-family: 'Poppins', sans-serif; }
47
+ .hist-head { padding: 16px 20px; background: linear-gradient(180deg, #151515, #1a1a1a); color: #fff; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #222; }
48
+ .hist-head-title { font-weight: 700; letter-spacing: 1px; font-size: 16px; }
49
+
50
+ /* İSTATİSTİK KARTLARI */
51
+ .hist-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding: 16px 20px; background: #0f0f0f; border-bottom: 1px solid #1a1a1a; }
52
+ .stat-card { background: linear-gradient(145deg, #1a1a1a, #141414); border-radius: 14px; padding: 14px 12px; text-align: center; border: 1px solid #222; }
53
+ .stat-value { font-size: 24px; font-weight: 700; color: #C5A065; margin-bottom: 4px; }
54
+ .stat-label { font-size: 11px; color: #777; text-transform: uppercase; letter-spacing: 0.5px; }
55
+
56
+ /* FİLTRELER */
57
+ .hist-filters { padding: 14px 20px; background: #0d0d0d; border-bottom: 1px solid #1a1a1a; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
58
+ .filter-group { display: flex; flex-direction: column; gap: 4px; }
59
+ .filter-group label { font-size: 10px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
60
+ .filter-input { background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 10px; padding: 10px 14px; color: #fff; font-size: 13px; min-width: 130px; }
61
+ .filter-input:focus { outline: none; border-color: #C5A065; }
62
+ .filter-input::placeholder { color: #555; }
63
+
64
+ .filter-select { background: #1a1a1a; border: 1px solid #2a2a2a; border-radius: 10px; padding: 10px 14px; color: #fff; font-size: 13px; min-width: 140px; cursor: pointer; }
65
+ .filter-select:focus { outline: none; border-color: #C5A065; }
66
+
67
+ .filter-btn { background: #C5A065; color: #000; border: none; border-radius: 10px; padding: 10px 16px; font-weight: 600; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 6px; }
68
+ .filter-btn:hover { filter: brightness(1.1); }
69
+ .filter-btn.secondary { background: #2a2a2a; color: #fff; }
70
+ .filter-btn.secondary:hover { background: #333; }
71
+
72
+ /* CSV BUTONLARI */
73
+ .action-buttons { margin-left: auto; display: flex; gap: 8px; }
74
+ .btn-csv { background: linear-gradient(145deg, #2E7D32, #1B5E20); color: #fff; border: none; border-radius: 10px; padding: 10px 16px; font-weight: 600; font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 6px; }
75
+ .btn-csv:hover { filter: brightness(1.1); }
76
+
77
+ /* LİSTE */
78
+ .hist-body { flex: 1; overflow-y: auto; padding: 10px 0; list-style: none; margin: 0; }
79
+ .hist-row { display: flex; align-items: center; padding: 14px 20px; margin: 8px 16px; border-radius: 14px; background: linear-gradient(145deg, #161616, #121212); border: 1px solid #1f1f1f; transition: transform 0.15s, border-color 0.15s; }
80
+ .hist-row:hover { transform: translateX(4px); border-color: #333; }
81
+ .hist-u-img { width: 48px; height: 48px; border-radius: 50%; margin-right: 14px; border: 2px solid #2a2a2a; object-fit: cover; background: #1a1a1a; }
82
+ .hist-info { flex: 1; }
83
+ .hist-u-name { display: block; font-weight: 700; font-size: 14px; color: #f0f0f0; margin-bottom: 3px; }
84
+ .hist-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
85
+ .hist-time { font-size: 12px; color: #666; }
86
+ .hist-staff { font-size: 11px; color: #888; background: #1f1f1f; padding: 3px 8px; border-radius: 6px; }
87
+ .hist-reward-badge { font-size: 11px; color: #C5A065; background: rgba(197, 160, 101, 0.15); padding: 3px 8px; border-radius: 6px; border: 1px solid rgba(197, 160, 101, 0.3); }
88
+ .hist-val { background: rgba(198,40,40,0.15); border: 1px solid rgba(198,40,40,0.3); color: #ef5350; padding: 10px 14px; border-radius: 12px; font-size: 14px; font-weight: 700; min-width: 70px; text-align: center; }
89
+
90
+ /* BOŞ DURUM */
91
+ .hist-empty { text-align: center; padding: 60px 20px; color: #555; list-style: none; }
92
+ .hist-empty i { font-size: 48px; margin-bottom: 16px; display: block; opacity: 0.5; }
93
+
94
+ /* LOADING */
95
+ .hist-loading { text-align: center; padding: 40px; color: #666; list-style: none; }
96
+ .hist-loading .spinner { display: inline-block; width: 30px; height: 30px; border: 3px solid #333; border-top-color: #C5A065; border-radius: 50%; animation: spin 1s linear infinite; }
97
+ @keyframes spin { to { transform: rotate(360deg); } }
98
+
99
+ /* UI/UX */
100
+ body.niki-kasa-body { -webkit-font-smoothing: antialiased; touch-action: manipulation; }
101
+ #content { min-height: 100vh; }
102
+ .start-view, .camera-ui, .hist-modal { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); }
103
+ .start-view { background: radial-gradient(1200px 700px at 50% 10%, rgba(197,160,101,0.18), transparent 60%), linear-gradient(180deg, #0b0b0b, #111 55%, #0b0b0b); }
104
+ .btn-group { display: grid; gap: 12px; }
105
+ .btn-start, .btn-hist, .btn-new, .btn-x { -webkit-tap-highlight-color: transparent; }
106
+ .btn-start:active, .btn-hist:active, .btn-new:active, .btn-x:active { transform: scale(0.97); }
107
+ .btn-start { width: min(320px, 88vw); border-radius: 16px; transition: transform 120ms ease; }
108
+ .btn-hist { width: min(320px, 88vw); border-radius: 16px; transition: transform 120ms ease; }
109
+ .top-ctrl { top: calc(14px + env(safe-area-inset-top)); padding: 0 16px; }
110
+ .btn-x { border: 1px solid rgba(255,255,255,0.15); }
111
+ .scan-guide { width: min(76vw, 320px); height: min(76vw, 320px); box-shadow: 0 0 0 5000px rgba(0,0,0,0.78); }
112
+ .scan-hint { bottom: calc(56px + env(safe-area-inset-bottom)); }
113
+ .res-modal { background: linear-gradient(180deg, #1c1c1c, #121212); border-top: 1px solid rgba(255,255,255,0.1); box-shadow: 0 -20px 50px rgba(0,0,0,0.6); padding-bottom: calc(20px + env(safe-area-inset-bottom)); }
114
+ .res-modal::before { content: ""; display: block; width: 44px; height: 5px; background: rgba(255,255,255,0.18); border-radius: 99px; margin: -10px auto 16px; }
115
+ .res-close { position: absolute; top: 14px; right: 14px; width: 42px; height: 42px; border-radius: 50%; background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.18); color: #fff; display:flex; align-items:center; justify-content:center; cursor:pointer; backdrop-filter: blur(6px); }
116
+ .res-close:active { transform: scale(0.95); }
117
+ .hist-body { padding-bottom: calc(16px + env(safe-area-inset-bottom)); }
118
+ @media (prefers-reduced-motion: reduce) { .laser { animation: none; } }
119
+ @media (max-width: 600px) {
120
+ .hist-stats { grid-template-columns: repeat(2, 1fr); }
121
+ .hist-filters { flex-direction: column; align-items: stretch; }
122
+ .filter-group { width: 100%; }
123
+ .filter-input, .filter-select { width: 100%; }
124
+ .action-buttons { width: 100%; justify-content: stretch; margin-left: 0; margin-top: 8px; }
125
+ .action-buttons .btn-csv { flex: 1; justify-content: center; }
126
+ }
127
+ </style>
128
+
129
+ <div id="start-view" class="start-view">
130
+ <div class="logo-box"><i class="fa fa-qrcode" style="font-size:45px; color:#C5A065;"></i></div>
131
+ <h2 style="margin:0 0 5px; font-size:24px;">NIKI POS</h2>
132
+ <p style="color:#888; font-size:13px; margin:0 0 40px;">Yetkili Personel Terminali</p>
133
+ <div class="btn-group">
134
+ <button class="btn-start" onclick="initSession()"><i class="fa fa-camera"></i> OTURUMU BAŞLAT</button>
135
+ <button class="btn-hist" onclick="openHistory()"><i class="fa fa-history"></i> İŞLEM GEÇMİŞİ</button>
136
+ </div>
137
+ </div>
138
+
139
+ <div id="camera-ui" class="camera-ui">
140
+ <div class="session-bar-bg"><div class="session-bar-fill" id="timer-bar"></div></div>
141
+ <div class="top-ctrl">
142
+ <div class="badge-live">● CANLI</div>
143
+ <button class="btn-x" onclick="stopSession()"><i class="fa fa-times"></i></button>
144
+ </div>
145
+ <div id="reader"></div>
146
+ <div class="scan-guide"><div class="laser"></div></div>
147
+ <div class="scan-hint">QR Kodu karenin içine tutun</div>
148
+ </div>
149
+
150
+ <div id="res-view" class="res-modal">
151
+ <button class="res-close" onclick="stopSession()" aria-label="Kapat"><i class="fa fa-times"></i></button>
152
+ <div id="res-icon" class="icon-big"></div>
153
+ <div id="res-title" class="res-title">...</div>
154
+ <div id="res-msg" class="res-msg">...</div>
155
+ <button class="btn-new" onclick="resumeScan()">YENİ İŞLEM</button>
156
+ </div>
157
+
158
+ <!-- GELİŞMİŞ İŞLEM GEÇMİŞİ -->
159
+ <div id="hist-view" class="hist-modal">
160
+ <div class="hist-head">
161
+ <span class="hist-head-title"><i class="fa fa-history"></i> İŞLEM GEÇMİŞİ</span>
162
+ <button onclick="closeHistory()" style="background:none; border:none; color:#fff; font-size:20px; cursor:pointer;"><i class="fa fa-times"></i></button>
163
+ </div>
164
+
165
+ <!-- İSTATİSTİKLER -->
166
+ <div class="hist-stats">
167
+ <div class="stat-card">
168
+ <div class="stat-value" id="stat-total">0</div>
169
+ <div class="stat-label">Toplam İşlem</div>
170
+ </div>
171
+ <div class="stat-card">
172
+ <div class="stat-value" id="stat-points">0</div>
173
+ <div class="stat-label">Toplam Puan</div>
174
+ </div>
175
+ <div class="stat-card">
176
+ <div class="stat-value" id="stat-today">0</div>
177
+ <div class="stat-label">Bugün</div>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- FİLTRELER -->
182
+ <div class="hist-filters">
183
+ <div class="filter-group">
184
+ <label>Başlangıç</label>
185
+ <input type="date" class="filter-input" id="filter-start">
186
+ </div>
187
+ <div class="filter-group">
188
+ <label>Bitiş</label>
189
+ <input type="date" class="filter-input" id="filter-end">
190
+ </div>
191
+ <div class="filter-group">
192
+ <label>Ara</label>
193
+ <input type="text" class="filter-input" id="filter-search" placeholder="Kullanıcı adı...">
194
+ </div>
195
+ <div class="filter-group">
196
+ <label>Ödül Tipi</label>
197
+ <select class="filter-select" id="filter-reward">
198
+ <option value="all">Tümü</option>
199
+ </select>
200
+ </div>
201
+ <button class="filter-btn" onclick="applyFilters()"><i class="fa fa-filter"></i> Filtrele</button>
202
+ <button class="filter-btn secondary" onclick="clearFilters()"><i class="fa fa-refresh"></i> Sıfırla</button>
203
+
204
+ <div class="action-buttons">
205
+ <button class="btn-csv" onclick="exportCSV()"><i class="fa fa-download"></i> CSV İndir</button>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- LİSTE -->
210
+ <ul id="hist-list" class="hist-body"></ul>
211
+ </div>
212
+
213
+ <div id="denied" style="display:none; text-align:center; padding:100px; color:#fff;"><h3>Yetkisiz Giriş</h3></div>
214
+
215
+ <script>
216
+ let html5QrCode;
217
+ let isProcessing = false;
218
+ let sessionTimer;
219
+ let currentHistoryData = [];
220
+ moment.locale('tr');
221
+
222
+ async function initSession() {
223
+ $('#start-view').fadeOut(200);
224
+ $('#camera-ui').fadeIn(300);
225
+ startTimer(600);
226
+ html5QrCode = new Html5Qrcode("reader");
227
+ try {
228
+ const devices = await Html5Qrcode.getCameras();
229
+ if (devices && devices.length) {
230
+ let cameraId = devices[devices.length - 1].id;
231
+ for (let i = 0; i < devices.length; i++) {
232
+ let label = devices[i].label.toLowerCase();
233
+ if (label.includes("back") || label.includes("arka") || label.includes("environment")) {
234
+ cameraId = devices[i].id;
235
+ break;
236
+ }
237
+ }
238
+ await html5QrCode.start(cameraId, { fps: 15, aspectRatio: window.innerWidth / window.innerHeight }, onScanSuccess);
239
+ } else {
240
+ alert("Kamera bulunamadı!");
241
+ stopSession();
242
+ }
243
+ } catch (err) {
244
+ alert("Kamera Başlatılamadı: " + err);
245
+ stopSession();
246
+ }
247
+ }
248
+
249
+ function startTimer(duration) {
250
+ let timeLeft = duration;
251
+ $('#timer-bar').css('width', '100%');
252
+ clearInterval(sessionTimer);
253
+ sessionTimer = setInterval(() => {
254
+ timeLeft--;
255
+ let pct = (timeLeft / duration) * 100;
256
+ $('#timer-bar').css('width', pct + '%');
257
+ if(timeLeft <= 0) { alert("Oturum doldu."); stopSession(); }
258
+ }, 1000);
259
+ }
260
+
261
+ function onScanSuccess(decodedText) {
262
+ if (isProcessing) return;
263
+ isProcessing = true;
264
+ const audio = new Audio('https://freesound.org/data/previews/171/171671_2437358-lq.mp3');
265
+ audio.play().catch(e=>{});
266
+ if(navigator.vibrate) navigator.vibrate(200);
267
+ html5QrCode.pause();
268
+ $.post('/api/niki-loyalty/scan-qr', { token: decodedText, _csrf: config.csrf_token }, function(res) {
269
+ if(res.success) {
270
+ $('#res-icon').html('<i class="fa fa-check-circle" style="color:#2E7D32"></i>');
271
+ $('#res-title').text('ÖDEME ALINDI');
272
+ $('#res-msg').html('<img src="'+(res.customer.picture||'')+'" style="width:60px;height:60px;border-radius:50%;border:2px solid #fff;margin-bottom:10px;"><br><b>'+res.customer.username+'</b><br>'+res.cost+' Puan Tahsil Edildi.<br><small style="color:#C5A065">'+res.rewardName+'</small>');
273
+ } else {
274
+ new Audio('https://freesound.org/data/previews/142/142608_1840739-lq.mp3').play().catch(e=>{});
275
+ if(navigator.vibrate) navigator.vibrate([100,50,100]);
276
+ $('#res-icon').html('<i class="fa fa-times-circle" style="color:#C62828"></i>');
277
+ $('#res-title').text('HATA');
278
+ $('#res-msg').text(res.message);
279
+ }
280
+ $('#res-view').addClass('active');
281
+ }).fail(function() { alert("Sunucu Hatası!"); resumeScan(); });
282
+ }
283
+
284
+ function resumeScan() {
285
+ $('#res-view').removeClass('active');
286
+ setTimeout(() => { isProcessing = false; html5QrCode.resume(); }, 300);
287
+ }
288
+
289
+ function stopSession() {
290
+ clearInterval(sessionTimer);
291
+ if(html5QrCode) html5QrCode.stop().then(() => html5QrCode.clear()).catch(()=>{});
292
+ $('#camera-ui').hide();
293
+ $('#res-view').removeClass('active');
294
+ $('#start-view').fadeIn(200);
295
+ isProcessing = false;
296
+ }
297
+
298
+ // =============================
299
+ // GELİŞMİŞ İŞLEM GEÇMİŞİ
300
+ // =============================
301
+
302
+ function openHistory() {
303
+ document.getElementById('hist-view').style.display = 'flex';
304
+ loadHistory();
305
+ }
306
+
307
+ function closeHistory() {
308
+ document.getElementById('hist-view').style.display = 'none';
309
+ }
310
+
311
+ function loadHistory(filters) {
312
+ filters = filters || {};
313
+ var list = document.getElementById('hist-list');
314
+ if (!list) { console.error('hist-list not found'); return; }
315
+
316
+ list.innerHTML = '<li class="hist-loading"><div class="spinner"></div><br>Yükleniyor...</li>';
317
+
318
+ var params = [];
319
+ if (filters.startDate) params.push('startDate=' + encodeURIComponent(filters.startDate));
320
+ if (filters.endDate) params.push('endDate=' + encodeURIComponent(filters.endDate));
321
+ if (filters.search) params.push('search=' + encodeURIComponent(filters.search));
322
+ if (filters.rewardType && filters.rewardType !== 'all') params.push('rewardType=' + encodeURIComponent(filters.rewardType));
323
+
324
+ var url = '/api/niki-loyalty/kasa-history' + (params.length ? '?' + params.join('&') : '');
325
+ console.log('[NIKI POS] Loading:', url);
326
+
327
+ $.get(url, function(response) {
328
+ console.log('[NIKI POS] Response:', response);
329
+
330
+ var dataToRender = [];
331
+ var stats = {};
332
+ var rewardTypes = [];
333
+
334
+ // Hem eski hem yeni format desteği
335
+ if (Array.isArray(response)) {
336
+ dataToRender = response;
337
+ stats = { totalTransactions: response.length, totalPoints: 0, byDate: {} };
338
+ } else if (response && response.data) {
339
+ dataToRender = response.data;
340
+ stats = response.stats || {};
341
+ rewardTypes = response.rewardTypes || [];
342
+ } else if (response && response.error) {
343
+ list.innerHTML = '<li class="hist-empty"><i class="fa fa-exclamation-circle"></i> Hata: ' + response.error + '</li>';
344
+ return;
345
+ }
346
+
347
+ currentHistoryData = dataToRender;
348
+
349
+ // İstatistikleri güncelle
350
+ var statTotal = document.getElementById('stat-total');
351
+ var statPoints = document.getElementById('stat-points');
352
+ var statToday = document.getElementById('stat-today');
353
+
354
+ if (statTotal) statTotal.textContent = stats.totalTransactions || dataToRender.length;
355
+ if (statPoints) statPoints.textContent = (stats.totalPoints || 0).toLocaleString('tr-TR');
356
+ if (statToday) {
357
+ var today = new Date().toISOString().slice(0, 10);
358
+ var byDate = stats.byDate || {};
359
+ statToday.textContent = byDate[today] || 0;
360
+ }
361
+
362
+ // Ödül tipi dropdown
363
+ var rewardSelect = document.getElementById('filter-reward');
364
+ if (rewardSelect && rewardTypes.length > 0) {
365
+ var currentVal = rewardSelect.value;
366
+ rewardSelect.innerHTML = '<option value="all">Tümü</option>';
367
+ rewardTypes.forEach(function(type) {
368
+ var opt = document.createElement('option');
369
+ opt.value = type;
370
+ opt.textContent = type;
371
+ rewardSelect.appendChild(opt);
372
+ });
373
+ if (currentVal) rewardSelect.value = currentVal;
374
+ }
375
+
376
+ // Liste render
377
+ renderHistoryList(dataToRender);
378
+
379
+ }).fail(function(xhr, status, error) {
380
+ console.error('[NIKI POS] Error:', error);
381
+ list.innerHTML = '<li class="hist-empty"><i class="fa fa-wifi"></i> Bağlantı hatası</li>';
382
+ });
383
+ }
384
+
385
+ function renderHistoryList(data) {
386
+ var list = document.getElementById('hist-list');
387
+ if (!list) return;
388
+
389
+ list.innerHTML = '';
390
+ console.log('[NIKI POS] Rendering', data.length, 'items');
391
+
392
+ if (!data || data.length === 0) {
393
+ list.innerHTML = '<li class="hist-empty"><i class="fa fa-inbox"></i> İşlem bulunamadı</li>';
394
+ return;
395
+ }
396
+
397
+ for (var i = 0; i < data.length; i++) {
398
+ var item = data[i];
399
+ var timeAgo = moment(item.ts).fromNow();
400
+ var exactTime = moment(item.ts).format('DD MMM YYYY, HH:mm');
401
+ var customerName = item.cust || 'Bilinmeyen';
402
+ var staffName = item.staffName || '';
403
+ var rewardName = item.reward || 'İşlem';
404
+ var amount = item.amt || 250;
405
+ var picture = item.picture || 'https://via.placeholder.com/50';
406
+
407
+ var staffBadge = staffName ? '<span class="hist-staff"><i class="fa fa-user-circle"></i> ' + escapeHtml(staffName) + '</span>' : '';
408
+
409
+ var li = document.createElement('li');
410
+ li.className = 'hist-row';
411
+ li.innerHTML = '<img src="' + picture + '" class="hist-u-img" onerror="this.src=\'https://via.placeholder.com/50\'">' +
412
+ '<div class="hist-info">' +
413
+ '<span class="hist-u-name">' + escapeHtml(customerName) + '</span>' +
414
+ '<div class="hist-meta">' +
415
+ '<span class="hist-time" title="' + exactTime + '">' + timeAgo + '</span>' +
416
+ staffBadge +
417
+ '<span class="hist-reward-badge">' + escapeHtml(rewardName) + '</span>' +
418
+ '</div>' +
419
+ '</div>' +
420
+ '<div class="hist-val">-' + amount + '</div>';
421
+ list.appendChild(li);
422
+ }
423
+ console.log('[NIKI POS] Render complete');
424
+ }
425
+
426
+ function applyFilters() {
427
+ loadHistory({
428
+ startDate: document.getElementById('filter-start').value,
429
+ endDate: document.getElementById('filter-end').value,
430
+ search: document.getElementById('filter-search').value,
431
+ rewardType: document.getElementById('filter-reward').value
432
+ });
433
+ }
434
+
435
+ function clearFilters() {
436
+ document.getElementById('filter-start').value = '';
437
+ document.getElementById('filter-end').value = '';
438
+ document.getElementById('filter-search').value = '';
439
+ document.getElementById('filter-reward').value = 'all';
440
+ loadHistory();
441
+ }
442
+
443
+ function exportCSV() {
444
+ var params = [];
445
+ var startDate = document.getElementById('filter-start').value;
446
+ var endDate = document.getElementById('filter-end').value;
447
+ var search = document.getElementById('filter-search').value;
448
+ var rewardType = document.getElementById('filter-reward').value;
449
+
450
+ if (startDate) params.push('startDate=' + encodeURIComponent(startDate));
451
+ if (endDate) params.push('endDate=' + encodeURIComponent(endDate));
452
+ if (search) params.push('search=' + encodeURIComponent(search));
453
+ if (rewardType && rewardType !== 'all') params.push('rewardType=' + encodeURIComponent(rewardType));
454
+ params.push('exportAll=true');
455
+
456
+ $.get('/api/niki-loyalty/kasa-history?' + params.join('&'), function(response) {
457
+ var data = response.data || response || [];
458
+ if (!data.length) { alert('Veri bulunamadı'); return; }
459
+
460
+ var headers = ['Tarih', 'Saat', 'Müşteri', 'Personel', 'Ödül', 'Puan'];
461
+ var rows = [];
462
+ for (var i = 0; i < data.length; i++) {
463
+ var item = data[i];
464
+ var date = moment(item.ts);
465
+ rows.push([
466
+ date.format('DD.MM.YYYY'),
467
+ date.format('HH:mm:ss'),
468
+ '"' + (item.cust || '').replace(/"/g, '""') + '"',
469
+ '"' + (item.staffName || '').replace(/"/g, '""') + '"',
470
+ '"' + (item.reward || '').replace(/"/g, '""') + '"',
471
+ item.amt || 0
472
+ ].join(';'));
473
+ }
474
+
475
+ var csv = '\uFEFF' + headers.join(';') + '\n' + rows.join('\n');
476
+ var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
477
+ var url = URL.createObjectURL(blob);
478
+ var link = document.createElement('a');
479
+ link.href = url;
480
+ link.download = 'niki-kasa-' + moment().format('YYYY-MM-DD') + '.csv';
481
+ document.body.appendChild(link);
482
+ link.click();
483
+ document.body.removeChild(link);
484
+ }).fail(function() { alert('CSV indirilemedi'); });
485
+ }
486
+
487
+ function escapeHtml(text) {
488
+ if (!text) return '';
489
+ var div = document.createElement('div');
490
+ div.textContent = text;
491
+ return div.innerHTML;
492
+ }
493
+
494
+ $(document).ready(function() {
495
+ if (!app.user.isAdmin && !app.user.isGlobalMod) {
496
+ $('.start-view').remove();
497
+ $('body').html('<div style="text-align:center; padding:50px; color:#fff;"><h3>⛔ Yetkisiz Giriş</h3></div>');
498
+ }
499
+ var today = new Date().toISOString().slice(0, 10);
500
+ var monthAgo = new Date(Date.now() - 30*24*60*60*1000).toISOString().slice(0, 10);
501
+ document.getElementById('filter-end').value = today;
502
+ document.getElementById('filter-start').value = monthAgo;
503
+ });
504
+
505
+ $(window).on('action:ajaxify.start', function() { stopSession(); closeHistory(); });
506
+ </script>