Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
H
haiyen-payment
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Giang Tran
haiyen-payment
Commits
a965ca8f
Commit
a965ca8f
authored
Sep 04, 2026
by
tdgiang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update code
parent
0e366893
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
181 additions
and
58 deletions
+181
-58
Screenshot 0008-09-04 at 09.03.51.png
Screenshot 0008-09-04 at 09.03.51.png
+0
-0
admin.server.controller.js
app/controllers/admin.server.controller.js
+124
-17
adminAuth.server.controller.js
app/controllers/adminAuth.server.controller.js
+25
-0
AdminUser.js
app/models/AdminUser.js
+1
-0
admin.server.routes.js
app/routes/admin.server.routes.js
+1
-0
accounts-list.server.view.html
app/views/admin/accounts-list.server.view.html
+11
-2
transactions-list.server.view.html
app/views/admin/transactions-list.server.view.html
+19
-39
Screenshot 0008-09-04 at 09.34.09.png
docs/Screenshot 0008-09-04 at 09.34.09.png
+0
-0
No files found.
Screenshot 0008-09-04 at 09.03.51.png
0 → 100644
View file @
a965ca8f
19.2 KB
app/controllers/admin.server.controller.js
View file @
a965ca8f
...
...
@@ -283,17 +283,25 @@ exports.epayIPN = function (req, res) {
};
var
TRANSACTION_STATUSES
=
[
"pending"
,
"success"
,
"failed"
];
var
DEFAULT_FEE_RATE
=
2.2
;
// Shared by listTransactions and exportTransactions so the two never drift apart.
function
buildTransactionFilter
(
req
)
{
// Staff scoping shared by the transaction table filter and the report cards:
// staff sees only their own transactions, admin sees everyone unless a specific staff is picked.
function
buildStaffScopeFilter
(
req
)
{
var
isAdmin
=
req
.
session
.
role
===
"admin"
;
var
filter
=
{};
if
(
!
isAdmin
)
{
filter
.
createdByUsername
=
req
.
session
.
username
;
}
else
if
(
req
.
query
.
staff
)
{
filter
.
createdByUsername
=
String
(
req
.
query
.
staff
);
}
return
filter
;
}
// Shared by listTransactions and exportTransactions so the two never drift apart.
function
buildTransactionFilter
(
req
)
{
var
isAdmin
=
req
.
session
.
role
===
"admin"
;
var
filter
=
buildStaffScopeFilter
(
req
);
if
(
TRANSACTION_STATUSES
.
indexOf
(
req
.
query
.
status
)
!==
-
1
)
{
filter
.
status
=
req
.
query
.
status
;
...
...
@@ -316,6 +324,97 @@ function buildTransactionFilter(req) {
};
}
function
sumSuccessAmount
(
match
)
{
return
AdminTransaction
.
aggregate
([
{
$match
:
match
},
{
$group
:
{
_id
:
null
,
total
:
{
$sum
:
"$amount"
}
}
},
])
.
exec
()
.
then
(
function
(
rows
)
{
return
rows
.
length
?
rows
[
0
].
total
:
0
;
});
}
// Bank không xử lý sao kê Thứ 7/CN: ngày thường lấy sản lượng thành công của hôm qua,
// riêng Thứ 2 gộp sản lượng thành công của Thứ 6 + Thứ 7 + CN tuần trước; Thứ 7/CN trả về null (=> 0).
function
settlementSourceRange
()
{
var
dayOfWeek
=
moment
().
day
();
// 0=CN, 1=T2 ... 6=T7
if
(
dayOfWeek
===
0
||
dayOfWeek
===
6
)
{
return
null
;
}
if
(
dayOfWeek
===
1
)
{
return
{
start
:
moment
().
subtract
(
3
,
"days"
).
startOf
(
"day"
).
toDate
(),
end
:
moment
().
subtract
(
1
,
"days"
).
endOf
(
"day"
).
toDate
(),
};
}
return
{
start
:
moment
().
subtract
(
1
,
"days"
).
startOf
(
"day"
).
toDate
(),
end
:
moment
().
subtract
(
1
,
"days"
).
endOf
(
"day"
).
toDate
(),
};
}
// Số tiền thanh toán = sản lượng thành công (ngày nguồn) - phí riêng của từng nhân viên,
// cộng dồn theo phạm vi (1 nhân viên nếu đang lọc, hoặc toàn bộ nếu admin không lọc ai).
function
computeSettlementAmount
(
scopeFilter
)
{
var
range
=
settlementSourceRange
();
if
(
!
range
)
{
return
Promise
.
resolve
(
0
);
}
var
match
=
Object
.
assign
({},
scopeFilter
,
{
status
:
"success"
,
createdAt
:
{
$gte
:
range
.
start
,
$lte
:
range
.
end
},
});
return
AdminTransaction
.
aggregate
([
{
$match
:
match
},
{
$group
:
{
_id
:
"$createdByUsername"
,
total
:
{
$sum
:
"$amount"
}
}
},
])
.
exec
()
.
then
(
function
(
rows
)
{
if
(
!
rows
.
length
)
return
0
;
var
usernames
=
rows
.
map
(
function
(
r
)
{
return
r
.
_id
;
}).
filter
(
Boolean
);
return
AdminUser
.
find
({
username
:
{
$in
:
usernames
}
},
"username feeRate"
)
.
exec
()
.
then
(
function
(
users
)
{
var
feeByUsername
=
{};
users
.
forEach
(
function
(
u
)
{
feeByUsername
[
u
.
username
]
=
u
.
feeRate
;
});
return
rows
.
reduce
(
function
(
sum
,
r
)
{
var
fee
=
feeByUsername
.
hasOwnProperty
(
r
.
_id
)
?
feeByUsername
[
r
.
_id
]
:
DEFAULT_FEE_RATE
;
return
sum
+
r
.
total
*
(
1
-
fee
/
100
);
},
0
);
});
});
}
function
computeReportStats
(
req
,
callback
)
{
var
scopeFilter
=
buildStaffScopeFilter
(
req
);
var
todayStart
=
moment
().
startOf
(
"day"
).
toDate
();
var
todayEnd
=
moment
().
endOf
(
"day"
).
toDate
();
var
monthStart
=
moment
().
startOf
(
"month"
).
toDate
();
Promise
.
all
([
sumSuccessAmount
(
Object
.
assign
({},
scopeFilter
,
{
status
:
"success"
,
createdAt
:
{
$gte
:
todayStart
,
$lte
:
todayEnd
}
})),
AdminTransaction
.
countDocuments
(
Object
.
assign
({},
scopeFilter
,
{
createdAt
:
{
$gte
:
todayStart
,
$lte
:
todayEnd
}
})).
exec
(),
sumSuccessAmount
(
Object
.
assign
({},
scopeFilter
,
{
status
:
"success"
,
createdAt
:
{
$gte
:
monthStart
,
$lte
:
todayEnd
}
})),
computeSettlementAmount
(
scopeFilter
),
])
.
then
(
function
(
results
)
{
callback
(
null
,
{
todayVolume
:
results
[
0
],
todayCount
:
results
[
1
],
monthVolume
:
results
[
2
],
settlementAmount
:
Math
.
round
(
results
[
3
]),
});
})
.
catch
(
function
(
err
)
{
callback
(
err
);
});
}
exports
.
listTransactions
=
function
(
req
,
res
)
{
var
page
=
parseInt
(
req
.
query
.
page
)
||
1
;
var
perPage
=
20
;
...
...
@@ -347,20 +446,28 @@ exports.listTransactions = function (req, res) {
paymentUrl
:
config
.
epay
.
req_domain
+
"/admin/pay/"
+
tx
.
merTrxId
,
};
});
res
.
render
(
"admin/transactions-list"
,
{
transactions
:
transactions
,
page
:
page
,
prevPage
:
page
-
1
,
nextPage
:
page
+
1
,
hasPrev
:
page
>
1
,
hasNext
:
transactions
.
length
===
perPage
,
isAdmin
:
isAdmin
,
username
:
req
.
session
.
username
,
staffOptions
:
staffOptions
||
[],
selectedStaff
:
req
.
query
.
staff
||
""
,
selectedStatus
:
built
.
selectedStatus
,
selectedFromDate
:
built
.
selectedFromDate
,
selectedToDate
:
built
.
selectedToDate
,
computeReportStats
(
req
,
function
(
statsErr
,
reportStats
)
{
if
(
statsErr
)
{
console
.
error
(
"listTransactions: report stats error:"
,
statsErr
.
message
);
reportStats
=
{
todayVolume
:
0
,
todayCount
:
0
,
monthVolume
:
0
,
settlementAmount
:
0
};
}
res
.
render
(
"admin/transactions-list"
,
{
transactions
:
transactions
,
page
:
page
,
prevPage
:
page
-
1
,
nextPage
:
page
+
1
,
hasPrev
:
page
>
1
,
hasNext
:
transactions
.
length
===
perPage
,
isAdmin
:
isAdmin
,
username
:
req
.
session
.
username
,
staffOptions
:
staffOptions
||
[],
selectedStaff
:
req
.
query
.
staff
||
""
,
selectedStatus
:
built
.
selectedStatus
,
selectedFromDate
:
built
.
selectedFromDate
,
selectedToDate
:
built
.
selectedToDate
,
reportStats
:
reportStats
,
});
});
});
}
...
...
app/controllers/adminAuth.server.controller.js
View file @
a965ca8f
...
...
@@ -82,6 +82,7 @@ exports.accountsList = function (req, res) {
error
:
req
.
query
.
error
||
null
,
created
:
req
.
query
.
created
===
"1"
,
reset
:
req
.
query
.
reset
===
"1"
,
feeUpdated
:
req
.
query
.
feeUpdated
===
"1"
,
});
});
};
...
...
@@ -204,6 +205,30 @@ exports.resetAccountPassword = function (req, res) {
});
};
exports
.
updateAccountFee
=
function
(
req
,
res
)
{
var
feeRate
=
parseFloat
(
req
.
body
.
feeRate
);
if
(
isNaN
(
feeRate
)
||
feeRate
<
0
||
feeRate
>
100
)
{
return
res
.
redirect
(
"/admin/accounts?error=INVALID_FEE"
);
}
AdminUser
.
findById
(
req
.
params
.
id
,
function
(
err
,
user
)
{
if
(
err
)
{
console
.
error
(
"updateAccountFee: DB error:"
,
err
.
message
);
return
res
.
redirect
(
"/admin/accounts?error=DB_ERROR"
);
}
if
(
!
user
)
{
return
res
.
status
(
404
).
send
(
"Không tìm thấy tài khoản"
);
}
user
.
feeRate
=
feeRate
;
user
.
save
(
function
(
saveErr
)
{
if
(
saveErr
)
{
console
.
error
(
"updateAccountFee: DB error saving:"
,
saveErr
.
message
);
return
res
.
redirect
(
"/admin/accounts?error=DB_ERROR"
);
}
return
res
.
redirect
(
"/admin/accounts?feeUpdated=1"
);
});
});
};
exports
.
changePasswordForm
=
function
(
req
,
res
)
{
res
.
render
(
"admin/change-password"
,
{
error
:
null
,
success
:
false
});
};
...
...
app/models/AdminUser.js
View file @
a965ca8f
...
...
@@ -8,6 +8,7 @@ var AdminUserSchema = new Schema(
passwordHash
:
{
type
:
String
,
required
:
true
},
role
:
{
type
:
String
,
enum
:
[
"admin"
,
"staff"
],
default
:
"staff"
},
active
:
{
type
:
Boolean
,
default
:
true
},
feeRate
:
{
type
:
Number
,
default
:
2.2
},
// % phí thanh toán áp lên sản lượng giao dịch thành công
},
{
timestamps
:
true
}
);
...
...
app/routes/admin.server.routes.js
View file @
a965ca8f
...
...
@@ -20,6 +20,7 @@ module.exports = function (app) {
app
.
route
(
"/admin/accounts/:id/toggle"
).
post
(
requireLogin
,
requireAdmin
,
adminAuth
.
toggleAccount
);
app
.
route
(
"/admin/accounts/:id/reset-password"
).
post
(
requireLogin
,
requireAdmin
,
adminAuth
.
resetAccountPassword
);
app
.
route
(
"/admin/accounts/:id/fee"
).
post
(
requireLogin
,
requireAdmin
,
adminAuth
.
updateAccountFee
);
app
.
route
(
"/admin/account/password"
)
.
get
(
requireLogin
,
adminAuth
.
changePasswordForm
)
...
...
app/views/admin/accounts-list.server.view.html
View file @
a965ca8f
...
...
@@ -97,6 +97,8 @@
{% if created %}
<div
class=
"alert alert-success"
>
Đã tạo tài khoản thành công.
</div>
{% endif %}
{% if reset %}
<div
class=
"alert alert-success"
>
Đã đặt lại mật khẩu.
</div>
{% endif %}
{% if feeUpdated %}
<div
class=
"alert alert-success"
>
Đã cập nhật phí thanh toán.
</div>
{% endif %}
{% if error == "INVALID_FEE" %}
<div
class=
"alert alert-error"
>
Phí thanh toán không hợp lệ (0 - 100).
</div>
{% endif %}
{% if error == "MISSING_FIELDS" %}
<div
class=
"alert alert-error"
>
Vui lòng nhập đủ thông tin.
</div>
{% endif %}
{% if error == "DUPLICATE_USERNAME" %}
<div
class=
"alert alert-error"
>
Tên đăng nhập đã tồn tại.
</div>
{% endif %}
{% if error == "DB_ERROR" %}
<div
class=
"alert alert-error"
>
Lỗi cơ sở dữ liệu, vui lòng thử lại.
</div>
{% endif %}
...
...
@@ -136,6 +138,7 @@
<th>
Tên đăng nhập
</th>
<th>
Vai trò
</th>
<th>
Trạng thái
</th>
<th>
Phí (%)
</th>
<th>
Thao tác
</th>
</tr>
</thead>
...
...
@@ -146,15 +149,21 @@
<td><span
class=
"pill pill-{{u.role}}"
>
{% if u.role == "admin" %}Admin{% else %}Nhân viên{% endif %}
</span></td>
<td><span
class=
"pill pill-{% if u.active %}active{% else %}locked{% endif %}"
>
{% if u.active %}Hoạt động{% else %}Đã khoá{% endif %}
</span></td>
<td>
<form
method=
"POST"
action=
"/admin/accounts/{{u.id}}/fee"
style=
"display:flex;gap:6px;align-items:center;"
>
<input
type=
"number"
name=
"feeRate"
value=
"{{u.feeRate}}"
min=
"0"
max=
"100"
step=
"0.1"
style=
"width:70px;"
required
>
<button
type=
"submit"
class=
"secondary"
>
Lưu
</button>
</form>
</td>
<td>
<div
class=
"row-actions"
>
{% if u.username == currentUsername %}
<span
style=
"font-size:12.5px;color:var(--color-text-muted);"
>
Tài khoản của bạn
</span>
{% else %}
<form
method=
"POST"
action=
"/admin/accounts/{{u.
_
id}}/toggle"
>
<form
method=
"POST"
action=
"/admin/accounts/{{u.id}}/toggle"
>
<button
type=
"submit"
class=
"secondary"
>
{% if u.active %}Khoá{% else %}Mở khoá{% endif %}
</button>
</form>
{% endif %}
<form
method=
"POST"
action=
"/admin/accounts/{{u.
_
id}}/reset-password"
>
<form
method=
"POST"
action=
"/admin/accounts/{{u.id}}/reset-password"
>
<input
type=
"password"
name=
"newPassword"
placeholder=
"Mật khẩu mới"
required
minlength=
"8"
>
<button
type=
"submit"
class=
"secondary"
>
Đặt lại
</button>
</form>
...
...
app/views/admin/transactions-list.server.view.html
View file @
a965ca8f
...
...
@@ -500,6 +500,25 @@
</div>
</div>
<div
class=
"stats"
id=
"statsStrip"
>
<div
class=
"stat-card is-revenue"
>
<p
class=
"stat-label"
>
Sản lượng giao dịch hôm nay
</p>
<p
class=
"stat-value"
>
{{reportStats.todayVolume|currency}} ₫
</p>
</div>
<div
class=
"stat-card"
>
<p
class=
"stat-label"
>
Tổng số giao dịch hôm nay
</p>
<p
class=
"stat-value"
>
{{reportStats.todayCount}}
</p>
</div>
<div
class=
"stat-card is-success"
>
<p
class=
"stat-label"
>
Số tiền thanh toán
</p>
<p
class=
"stat-value"
>
{{reportStats.settlementAmount|currency}} ₫
</p>
</div>
<div
class=
"stat-card is-revenue"
>
<p
class=
"stat-label"
>
Tổng sản lượng tháng này
</p>
<p
class=
"stat-value"
>
{{reportStats.monthVolume|currency}} ₫
</p>
</div>
</div>
<form
class=
"filter-bar"
method=
"GET"
action=
"/admin/transactions"
>
{% if isAdmin %}
<select
name=
"staff"
class=
"select-control"
aria-label=
"Lọc theo nhân viên"
>
...
...
@@ -526,27 +545,6 @@
{% endif %}
</form>
{% if transactions.length %}
<div
class=
"stats"
id=
"statsStrip"
>
<div
class=
"stat-card"
>
<p
class=
"stat-label"
>
Tổng số GD
</p>
<p
class=
"stat-value"
data-stat=
"total"
>
0
</p>
</div>
<div
class=
"stat-card is-success"
>
<p
class=
"stat-label"
>
Thành công
</p>
<p
class=
"stat-value"
data-stat=
"success"
>
0
</p>
</div>
<div
class=
"stat-card is-pending"
>
<p
class=
"stat-label"
>
Chờ thanh toán
</p>
<p
class=
"stat-value"
data-stat=
"pending"
>
0
</p>
</div>
<div
class=
"stat-card is-revenue"
>
<p
class=
"stat-label"
>
Tổng tiền thành công
</p>
<p
class=
"stat-value"
data-stat=
"revenue"
>
0 ₫
</p>
</div>
</div>
{% endif %}
<div
class=
"card"
>
{% if transactions.length %}
<div
class=
"toolbar"
>
...
...
@@ -621,24 +619,6 @@
var
rows
=
Array
.
prototype
.
slice
.
call
(
document
.
querySelectorAll
(
'#txTableBody tr'
));
if
(
!
rows
.
length
)
return
;
var
counts
=
{
total
:
rows
.
length
,
success
:
0
,
pending
:
0
};
var
successRevenue
=
0
;
rows
.
forEach
(
function
(
row
)
{
var
status
=
row
.
getAttribute
(
'data-status'
);
if
(
status
===
'success'
)
{
counts
.
success
++
;
successRevenue
+=
parseInt
(
row
.
getAttribute
(
'data-amount'
),
10
)
||
0
;
}
else
if
(
status
!==
'failed'
)
{
counts
.
pending
++
;
}
});
Object
.
keys
(
counts
).
forEach
(
function
(
key
)
{
var
el
=
document
.
querySelector
(
'[data-stat="'
+
key
+
'"]'
);
if
(
el
)
el
.
textContent
=
counts
[
key
].
toLocaleString
(
'vi-VN'
);
});
var
revenueEl
=
document
.
querySelector
(
'[data-stat="revenue"]'
);
if
(
revenueEl
)
revenueEl
.
textContent
=
successRevenue
.
toLocaleString
(
'vi-VN'
)
+
' ₫'
;
var
searchInput
=
document
.
getElementById
(
'searchInput'
);
var
visibleCountEl
=
document
.
getElementById
(
'visibleCount'
);
var
noResultsEl
=
document
.
getElementById
(
'noSearchResults'
);
...
...
docs/Screenshot 0008-09-04 at 09.34.09.png
0 → 100644
View file @
a965ca8f
10.3 KB
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment