diff --git a/apps/admin/src/i18n/admin-pages-ms.ts b/apps/admin/src/i18n/admin-pages-ms.ts index fcead04..c1ee8ea 100644 --- a/apps/admin/src/i18n/admin-pages-ms.ts +++ b/apps/admin/src/i18n/admin-pages-ms.ts @@ -63,6 +63,14 @@ export const adminPagesMs: Record = { 'user.page_settings': 'Tetapan global', 'user.global_settings': 'Kata laluan & akaun (global)', 'user.global_settings_hint': 'Kawal sama ada semua pemain boleh ubah kata laluan/nama akaun dalam app', + 'user.reset_database': 'Set semula pangkalan data', + 'user.reset_database_hint': 'Padam semua data perniagaan dan pulihkan data demo awal. Tidak boleh dibatalkan.', + 'user.reset_database_confirm_label': 'Taip RESET untuk sahkan', + 'user.reset_database_confirm_ph': 'RESET', + 'user.reset_database_btn': 'Set semula ke data awal', + 'user.reset_database_disabled_prod': 'Dilumpuhkan dalam produksi melainkan ALLOW_DB_RESET=true', + 'user.reset_database_success': 'Pangkalan data diset semula. Sila log masuk semula.', + 'user.reset_database_accounts': 'Akaun demo', 'user.section.password_mgmt': 'Pengurusan kata laluan', 'user.field.current_password': 'Kata laluan semasa', 'user.msg.created_with_password': 'Pemain dicipta. Kata laluan: {password}', @@ -226,7 +234,24 @@ export const adminPagesMs: Record = { 'cashback.stat.players': 'Pemain', 'cashback.stat.total': 'Jumlah rebat', 'cashback.stat.lines': 'Baris butiran', + 'cashback.stat.effective_stake': 'Jumlah stake berkesan', + 'cashback.stat.bet_count': 'Bil. pertaruhan', + 'cashback.stat.avg_rate': 'Kadar purata', 'cashback.batch_no': 'No. kelompok', + 'cashback.history_title': 'Rekod rebat', + 'cashback.history_empty': 'Tiada kelompok rebat', + 'cashback.filter_status': 'Status', + 'cashback.status.PREVIEW': 'Menunggu', + 'cashback.status.CONFIRMED': 'Dibayar', + 'cashback.col.period': 'Tempoh', + 'cashback.col.status': 'Status', + 'cashback.col.bet_count': 'Pertaruhan', + 'cashback.col.created_at': 'Dicipta', + 'cashback.col.confirmed_at': 'Dibayar pada', + 'cashback.col.operator': 'Operator', + 'cashback.view_detail': 'Butiran', + 'cashback.detail_title': 'Butiran kelompok', + 'cashback.detail_summary': 'Ringkasan kelompok', 'cashback.table_title': 'Butiran rebat pemain', 'cashback.table_total': 'Jumlah', 'cashback.empty_items': 'Tiada rebat layak dalam tempoh ini', @@ -237,12 +262,16 @@ export const adminPagesMs: Record = { 'cashback.col.rate': 'Kadar', 'cashback.col.amount': 'Rebat', 'cashback.confirm_issue': 'Sahkan bayaran', + 'cashback.cancel_issue': 'Batalkan', + 'cashback.confirm_prompt': 'Bayar rebat kelompok ini ke dompet pemain? Tindakan ini tidak boleh dibatalkan.', + 'cashback.cancel_prompt': 'Batalkan kelompok menunggu ini? Tiada kredit dompet; boleh pratonton semula.', + 'cashback.status.CANCELLED': 'Dibatalkan', 'cashback.rules_title': 'Peraturan rebat', 'cashback.rule_period': 'Pilih julat tarikh. Taruhan dikira mengikut masa penyelesaian dalam tempoh tersebut.', 'cashback.rule_eligible': 'Termasuk: taruhan selesai WON/LOST (tunggal ikut stake; parlay sekali ikut stake parlay). Tidak termasuk: belum selesai, dibatalkan, batal, push, dan kadar 0.', 'cashback.rule_formula': 'Setiap taruhan: stake × kadar rebat. Jumlah diagregat mengikut pemain.', 'cashback.rule_rate': 'Keutamaan kadar: pemain > ejen > global > kadar lalai ejen (cth. 0.01 = 1%).', - 'cashback.rule_flow': 'Aliran: pratonton → semak jumlah → sahkan bayaran (kredit dompet, entri CASHBACK).', + 'cashback.rule_flow': 'Aliran: pratonton (satu menunggu setiap tempoh) → semak → sahkan bayaran; batalkan jika tidak perlu. Tempoh dibayar tidak boleh pratonton semula.', 'cashback.rule_note_zero': 'Jika 0, semak taruhan WON/LOST dalam tempoh dan kadar rebat > 0.', 'user.field.player_id': 'ID pemain', @@ -532,6 +561,9 @@ export const adminPagesMs: Record = { 'msg.outright_teams_added': '{n} pasukan ditambah ({skipped} dilangkau)', 'msg.load_matches_failed': 'Gagal memuatkan perlawanan', 'msg.cashback_issued': 'Rebat telah dikeluarkan', + 'msg.cashback_cancelled': 'Kelompok rebat dibatalkan', + 'msg.cashback_preview_ready': 'Pratonton sedia — semak dan sahkan bayaran', + 'msg.cashback_preview_replaced': 'Menggantikan {n} pratonton lama untuk tempoh ini', 'msg.freeze_confirm_title': '{action} akaun', 'msg.freeze_confirm_body': '{action} pemain "{name}"?{extra}', 'msg.freeze_extra': ' Mereka tidak akan dapat log masuk.', diff --git a/apps/admin/src/i18n/admin-pages.ts b/apps/admin/src/i18n/admin-pages.ts index 95a42e4..1b8c8b5 100644 --- a/apps/admin/src/i18n/admin-pages.ts +++ b/apps/admin/src/i18n/admin-pages.ts @@ -63,6 +63,14 @@ export const adminPagesZh: Record = { 'user.page_settings': '全局设置', 'user.global_settings': '密码与账号管理(全局)', 'user.global_settings_hint': '控制所有玩家是否可在 App 内改密码、改账号名', + 'user.reset_database': '重置数据库', + 'user.reset_database_hint': '清空全部业务数据并恢复为初始演示数据(用户、赛事、注单、账变等)。此操作不可撤销。', + 'user.reset_database_confirm_label': '请输入 RESET 以确认', + 'user.reset_database_confirm_ph': '输入 RESET', + 'user.reset_database_btn': '重置为初始数据', + 'user.reset_database_disabled_prod': '生产环境已禁用;需服务端设置 ALLOW_DB_RESET=true', + 'user.reset_database_success': '数据库已重置,请使用初始账号重新登录', + 'user.reset_database_accounts': '演示账号', 'user.section.password_mgmt': '密码管理', 'user.field.current_password': '当前密码', 'user.msg.created_with_password': '玩家已创建,登录密码:{password}', @@ -215,6 +223,7 @@ export const adminPagesZh: Record = { 'audit.col.time': '时间', 'audit.action.CREATE_PLAYER': '新建玩家', 'audit.action.UPDATE_PLAYER': '更新玩家', + 'audit.action.RESET_DATABASE': '重置数据库', 'audit.action.CREATE_AGENT': '新建代理', 'audit.action.UPDATE_AGENT': '更新代理', 'audit.module.USERS': '玩家', @@ -227,7 +236,24 @@ export const adminPagesZh: Record = { 'cashback.stat.players': '涉及玩家数', 'cashback.stat.total': '返水总金额', 'cashback.stat.lines': '明细条数', + 'cashback.stat.effective_stake': '有效投注总额', + 'cashback.stat.bet_count': '计入注单数', + 'cashback.stat.avg_rate': '平均返水比例', 'cashback.batch_no': '批次号', + 'cashback.history_title': '返水记录', + 'cashback.history_empty': '暂无返水批次记录', + 'cashback.filter_status': '批次状态', + 'cashback.status.PREVIEW': '待发放', + 'cashback.status.CONFIRMED': '已发放', + 'cashback.col.period': '统计周期', + 'cashback.col.status': '状态', + 'cashback.col.bet_count': '注单数', + 'cashback.col.created_at': '生成时间', + 'cashback.col.confirmed_at': '发放时间', + 'cashback.col.operator': '操作人', + 'cashback.view_detail': '查看明细', + 'cashback.detail_title': '返水批次明细', + 'cashback.detail_summary': '批次汇总', 'cashback.table_title': '玩家返水明细', 'cashback.table_total': '合计', 'cashback.empty_items': '本周期内无符合条件的返水记录', @@ -238,12 +264,16 @@ export const adminPagesZh: Record = { 'cashback.col.rate': '返水比例', 'cashback.col.amount': '返水金额', 'cashback.confirm_issue': '确认发放', + 'cashback.cancel_issue': '作废', + 'cashback.confirm_prompt': '确认向玩家钱包发放本批次返水?此操作不可撤销。', + 'cashback.cancel_prompt': '确认作废该待发放批次?作废后不会入账,可重新生成预览。', + 'cashback.status.CANCELLED': '已作废', 'cashback.rules_title': '返水规则说明', 'cashback.rule_period': '选择开始/结束日期,统计该周期内、按注单结算时间落在区间内的有效投注。', 'cashback.rule_eligible': '计入:已结算且结果为「赢」或「输」的注单(单关按本金,串关按整单本金计一次)。不计入:未结算、已取消、作废、走水,以及返水比例为 0 的注单。', 'cashback.rule_formula': '单笔返水 = 投注本金 × 适用返水比例;同一玩家多笔注单汇总后生成一条返水明细。', 'cashback.rule_rate': '返水比例优先级:玩家专属规则 > 代理线规则 > 全局规则 > 所属代理默认返水率(在代理/玩家管理中配置,如 0.01 表示 1%)。', - 'cashback.rule_flow': '操作流程:生成预览 → 核对涉及玩家数与总金额 → 确认发放(金额入账玩家钱包,并生成 CASHBACK 账变流水)。', + 'cashback.rule_flow': '操作流程:生成预览(同周期仅保留一条待发放)→ 核对明细 → 确认发放;不需要的可作废。已发放周期不可重复预览。', 'cashback.rule_note_zero': '预览为 0 时,请检查:周期内是否有已结算输赢注单、代理/玩家是否配置了大于 0 的返水率。', 'user.field.player_id': '玩家 ID', @@ -593,6 +623,9 @@ export const adminPagesZh: Record = { 'outright.hidden_reason.MARKET_CLOSED': '冠军盘口未开放,请联系技术检查盘口状态。', 'msg.load_matches_failed': '加载赛事失败', 'msg.cashback_issued': '返水已发放', + 'msg.cashback_cancelled': '返水批次已作废', + 'msg.cashback_preview_ready': '预览已生成,请核对后确认发放', + 'msg.cashback_preview_replaced': '已替换同周期 {n} 条旧预览', 'msg.freeze_confirm_title': '{action}账号', 'msg.freeze_confirm_body': '确定要{action}玩家「{name}」吗?{extra}', 'msg.freeze_extra': '冻结后该账号将无法登录。', @@ -664,6 +697,14 @@ export const adminPagesEn: Record = { 'user.page_settings': 'Global settings', 'user.global_settings': 'Password & account (global)', 'user.global_settings_hint': 'Controls whether all players can change password or username in the app', + 'user.reset_database': 'Reset database', + 'user.reset_database_hint': 'Wipes all business data and restores initial demo seed (users, matches, bets, ledger, etc.). This cannot be undone.', + 'user.reset_database_confirm_label': 'Type RESET to confirm', + 'user.reset_database_confirm_ph': 'RESET', + 'user.reset_database_btn': 'Reset to initial data', + 'user.reset_database_disabled_prod': 'Disabled in production unless ALLOW_DB_RESET=true is set on the server', + 'user.reset_database_success': 'Database reset complete. Sign in again with demo accounts.', + 'user.reset_database_accounts': 'Demo accounts', 'user.section.password_mgmt': 'Password management', 'user.field.current_password': 'Current password', 'user.msg.created_with_password': 'Player created. Login password: {password}', @@ -816,6 +857,7 @@ export const adminPagesEn: Record = { 'audit.col.time': 'Time', 'audit.action.CREATE_PLAYER': 'Create player', 'audit.action.UPDATE_PLAYER': 'Update player', + 'audit.action.RESET_DATABASE': 'Reset database', 'audit.action.CREATE_AGENT': 'Create agent', 'audit.action.UPDATE_AGENT': 'Update agent', 'audit.module.USERS': 'Players', @@ -828,7 +870,24 @@ export const adminPagesEn: Record = { 'cashback.stat.players': 'Players', 'cashback.stat.total': 'Total cashback', 'cashback.stat.lines': 'Line items', + 'cashback.stat.effective_stake': 'Total effective stake', + 'cashback.stat.bet_count': 'Eligible bets', + 'cashback.stat.avg_rate': 'Average rate', 'cashback.batch_no': 'Batch no.', + 'cashback.history_title': 'Cashback history', + 'cashback.history_empty': 'No cashback batches yet', + 'cashback.filter_status': 'Status', + 'cashback.status.PREVIEW': 'Pending', + 'cashback.status.CONFIRMED': 'Paid out', + 'cashback.col.period': 'Period', + 'cashback.col.status': 'Status', + 'cashback.col.bet_count': 'Bets', + 'cashback.col.created_at': 'Created', + 'cashback.col.confirmed_at': 'Paid at', + 'cashback.col.operator': 'Operator', + 'cashback.view_detail': 'Details', + 'cashback.detail_title': 'Batch breakdown', + 'cashback.detail_summary': 'Batch summary', 'cashback.table_title': 'Player cashback breakdown', 'cashback.table_total': 'Total', 'cashback.empty_items': 'No eligible cashback in this period', @@ -839,12 +898,16 @@ export const adminPagesEn: Record = { 'cashback.col.rate': 'Rate', 'cashback.col.amount': 'Cashback', 'cashback.confirm_issue': 'Confirm payout', + 'cashback.cancel_issue': 'Void', + 'cashback.confirm_prompt': 'Pay out this cashback batch to player wallets? This cannot be undone.', + 'cashback.cancel_prompt': 'Void this pending batch? No wallet credit will be made; you can preview again.', + 'cashback.status.CANCELLED': 'Voided', 'cashback.rules_title': 'Cashback rules', 'cashback.rule_period': 'Pick a date range. Bets are included by settlement time within that period.', 'cashback.rule_eligible': 'Included: settled bets with result WON or LOST (singles by stake; parlays counted once by parlay stake). Excluded: pending, cancelled, void, push, and zero-rate bets.', 'cashback.rule_formula': 'Per bet: stake × applicable cashback rate. Amounts are summed per player into one line item.', 'cashback.rule_rate': 'Rate priority: player rule > agent rule > global rule > agent default rate (set under Agents/Players, e.g. 0.01 = 1%).', - 'cashback.rule_flow': 'Flow: preview → verify player count and total → confirm payout (credits wallet, creates CASHBACK ledger entries).', + 'cashback.rule_flow': 'Flow: preview (one pending batch per period) → review → confirm payout; void if not needed. Paid periods cannot be previewed again.', 'cashback.rule_note_zero': 'If preview is 0, check for settled WON/LOST bets in the period and a cashback rate above 0.', 'user.field.player_id': 'Player ID', @@ -1195,6 +1258,9 @@ export const adminPagesEn: Record = { 'outright.hidden_reason.MARKET_CLOSED': 'Winner market is not open.', 'msg.load_matches_failed': 'Failed to load matches', 'msg.cashback_issued': 'Cashback issued', + 'msg.cashback_cancelled': 'Cashback batch voided', + 'msg.cashback_preview_ready': 'Preview ready — review and confirm payout', + 'msg.cashback_preview_replaced': 'Replaced {n} older preview(s) for this period', 'msg.freeze_confirm_title': '{action} account', 'msg.freeze_confirm_body': '{action} player "{name}"?{extra}', 'msg.freeze_extra': ' They will not be able to sign in.', diff --git a/apps/admin/src/views/Cashback.vue b/apps/admin/src/views/Cashback.vue index 3e0fd89..66a1c9b 100644 --- a/apps/admin/src/views/Cashback.vue +++ b/apps/admin/src/views/Cashback.vue @@ -1,9 +1,10 @@ + + +
+
{{ t('cashback.history_title') }}
+ + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+ +
+
@@ -278,25 +621,44 @@ async function confirm() { margin-bottom: 16px; } -.preview-head { +.history-card { + margin-bottom: 0; +} + +.preview-head, +.history-head { display: flex; align-items: flex-start; justify-content: space-between; + gap: 12px; margin-bottom: 16px; } -.preview-title { - font-size: 15px; +.preview-title, +.table-title { + font-size: 14px; font-weight: 600; color: #e0e0e0; } -.preview-meta { +.preview-meta, +.detail-meta { margin-top: 6px; font-size: 12px; color: #888; } +.detail-tag { + margin-left: 8px; + vertical-align: middle; +} + +.detail-summary { + margin: 12px 0 8px; + font-size: 13px; + color: #aaa; +} + .meta-sep { margin: 0 6px; } @@ -305,16 +667,25 @@ async function confirm() { margin-bottom: 0; } +.detail-stats { + margin-bottom: 12px; +} + .pstat { - padding: 16px; + padding: 14px 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid #2a2a2a; border-radius: 10px; text-align: center; + margin-bottom: 8px; +} + +.pstat-compact { + padding: 12px 10px; } .pstat-value { - font-size: 26px; + font-size: 22px; font-weight: 700; color: #e0e0e0; } @@ -330,13 +701,6 @@ async function confirm() { margin-top: 4px; } -.table-title { - font-size: 14px; - font-weight: 600; - color: #e0e0e0; - margin-bottom: 12px; -} - .table-wrap { overflow-x: auto; } @@ -346,7 +710,17 @@ async function confirm() { font-weight: 600; } -.confirm-btn { +.preview-actions, +.detail-actions { + display: flex; + justify-content: flex-end; + gap: 10px; margin-top: 20px; } + +.pager { + margin-top: 16px; + display: flex; + justify-content: flex-end; +} diff --git a/apps/admin/src/views/Users.vue b/apps/admin/src/views/Users.vue index 2771b00..41bd257 100644 --- a/apps/admin/src/views/Users.vue +++ b/apps/admin/src/views/Users.vue @@ -1,11 +1,14 @@ + + + + diff --git a/apps/player/src/views/WalletView.vue b/apps/player/src/views/WalletView.vue index 835a15e..2a09f7a 100644 --- a/apps/player/src/views/WalletView.vue +++ b/apps/player/src/views/WalletView.vue @@ -1,19 +1,22 @@