Commit 40388425 by tdgiang

Redesign admin transaction management UI

Applies a cohesive visual system to the admin panel: a navy/blue
dashboard style (Inter font) for the internal staff pages, and a
navy/gold fintech-trust style (IBM Plex Sans) for the customer-facing
payment pages. Preserves all existing form fields, MegaPay integration
markup, and Swig template bindings unchanged.
parent 8cfa1225
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="vi">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kết quả thanh toán</title> <title>Kết quả thanh toán</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
body { font-family: Arial, sans-serif; max-width: 480px; margin: 60px auto; text-align: center; } :root {
--color-bg: #F1F5F9;
--color-surface: #FFFFFF;
--color-navy: #0F172A;
--color-navy-muted: #475569;
--color-border: #E2E8F0;
--color-success: #15803D;
--color-success-bg: #F0FDF4;
--color-success-border: #BBF7D0;
--color-danger: #B91C1C;
--color-danger-bg: #FEF2F2;
--color-danger-border: #FECACA;
--radius: 14px;
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 12px 32px rgba(15, 23, 42, 0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--color-bg);
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: var(--color-navy);
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
}
.card {
width: 100%;
max-width: 400px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 44px 32px;
text-align: center;
}
.state-icon {
width: 64px;
height: 64px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
animation: pop 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.state-icon.success { background: var(--color-success-bg); color: var(--color-success); }
.state-icon.failed { background: var(--color-danger-bg); color: var(--color-danger); }
@keyframes pop {
from { transform: scale(0.6); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
h1 {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 8px;
}
h1.success { color: var(--color-success); }
h1.failed { color: var(--color-danger); }
p.message {
font-size: 14px;
color: var(--color-navy-muted);
line-height: 1.6;
margin: 0;
}
@media (prefers-reduced-motion: reduce) {
.state-icon { animation: none; }
}
</style> </style>
</head> </head>
<body> <body>
<div class="card">
{% if success %} {% if success %}
<h2 style="color:#276b45">Thanh toán thành công</h2> <div class="state-icon success">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
</div>
<h1 class="success">Thanh toán thành công</h1>
<p class="message">Giao dịch của bạn đã được xử lý. Bạn có thể đóng trang này.</p>
{% else %} {% else %}
<h2 style="color:#a3352a">Thanh toán thất bại</h2> <div class="state-icon failed">
<p>{{message}}</p> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</div>
<h1 class="failed">Thanh toán thất bại</h1>
<p class="message">{{message}}</p>
{% endif %} {% endif %}
</div>
</body> </body>
</html> </html>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="vi">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Thanh toán</title> <title>Thanh toán</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
{% if not notFound %} {% if not notFound %}
<link rel="stylesheet" href="{{domain}}/pg_was/css/payment/layer/paymentClient.css"> <link rel="stylesheet" href="{{domain}}/pg_was/css/payment/layer/paymentClient.css">
{% endif %} {% endif %}
<style>
:root {
--color-bg: #F1F5F9;
--color-surface: #FFFFFF;
--color-navy: #0F172A;
--color-navy-muted: #475569;
--color-accent: #A16207;
--color-cta: #1D4ED8;
--color-cta-hover: #1E40AF;
--color-border: #E2E8F0;
--color-success: #15803D;
--color-success-bg: #F0FDF4;
--color-success-border: #BBF7D0;
--color-danger: #B91C1C;
--color-danger-bg: #FEF2F2;
--color-danger-border: #FECACA;
--radius: 14px;
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 12px 32px rgba(15, 23, 42, 0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--color-bg);
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: var(--color-navy);
display: flex;
justify-content: center;
align-items: flex-start;
padding: 48px 20px;
}
.page { width: 100%; max-width: 440px; }
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 24px;
color: var(--color-navy-muted);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 36px 32px;
}
.state-icon {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
}
.state-icon.neutral { background: #F1F5F9; color: var(--color-navy-muted); }
.state-icon.success { background: var(--color-success-bg); color: var(--color-success); }
h1 {
font-size: 20px;
font-weight: 700;
text-align: center;
letter-spacing: -0.01em;
margin: 0 0 8px;
}
.center-text {
text-align: center;
font-size: 14px;
color: var(--color-navy-muted);
line-height: 1.6;
margin: 0;
}
.amount-display {
text-align: center;
margin: 4px 0 24px;
}
.amount-display .label {
font-size: 13px;
color: var(--color-navy-muted);
margin-bottom: 4px;
}
.amount-display .value {
font-size: 32px;
font-weight: 700;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.summary {
background: #F8FAFC;
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 16px;
margin-bottom: 24px;
}
.summary-row {
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 14px;
padding: 6px 0;
}
.summary-row .k { color: var(--color-navy-muted); }
.summary-row .v { font-weight: 600; text-align: right; }
.pay-btn {
width: 100%;
height: 50px;
background: var(--color-cta);
color: #fff;
font-family: inherit;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background 150ms ease, transform 150ms ease;
}
.pay-btn:hover { background: var(--color-cta-hover); }
.pay-btn:active { transform: scale(0.99); }
.secure-note {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 16px;
font-size: 12.5px;
color: var(--color-navy-muted);
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
margin: 0 auto 16px;
}
.badge.success { background: var(--color-success-bg); color: var(--color-success); border: 1px solid var(--color-success-border); }
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}
</style>
</head> </head>
<body> <body>
<div class="page">
<div class="brand">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Thanh toán an toàn
</div>
<div class="card">
{% if notFound %} {% if notFound %}
<h2>Không tìm thấy giao dịch</h2> <div class="state-icon neutral">
<p>Đường dẫn thanh toán không hợp lệ hoặc đã hết hạn.</p> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</div>
<h1>Không tìm thấy giao dịch</h1>
<p class="center-text">Đường dẫn thanh toán không hợp lệ hoặc đã hết hạn. Vui lòng liên hệ người gửi để nhận lại đường dẫn mới.</p>
{% else %} {% else %}
{% if alreadyPaid %} {% if alreadyPaid %}
<h2>Giao dịch này đã được xử lý</h2> <div class="state-icon success">
<p>Trạng thái: {{tx.status}}</p> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
</div>
<h1>Giao dịch này đã được xử lý</h1>
<p class="center-text" style="margin-bottom: 20px;">Trạng thái hiện tại của giao dịch:</p>
<div style="text-align:center;">
<span class="badge success">{{tx.status}}</span>
</div>
{% else %} {% else %}
<h2>Thanh toán {{tx.amount}} VNĐ</h2> <div class="amount-display">
<p>Khách hàng: {{tx.customerName}}</p> <div class="label">Số tiền cần thanh toán</div>
<div class="value">{{tx.amount}} <span style="font-size:16px;font-weight:600;color:var(--color-navy-muted);">VNĐ</span></div>
</div>
<div class="summary">
<div class="summary-row">
<span class="k">Khách hàng</span>
<span class="v">{{tx.customerName}}</span>
</div>
<div class="summary-row">
<span class="k">Mã đơn hàng</span>
<span class="v">{{tx.transCode}}</span>
</div>
</div>
<form id="megapayForm" name="megapayForm" method="POST"> <form id="megapayForm" name="megapayForm" method="POST">
<input type="hidden" name="merId" value="{{merId}}"> <input type="hidden" name="merId" value="{{merId}}">
<input type="hidden" name="currency" value="VND"> <input type="hidden" name="currency" value="VND">
...@@ -36,10 +241,19 @@ ...@@ -36,10 +241,19 @@
<input type="hidden" name="windowColor" value="#ef5459"> <input type="hidden" name="windowColor" value="#ef5459">
<input type="hidden" name="windowType" value=""> <input type="hidden" name="windowType" value="">
</form> </form>
<button onclick="openPayment(1, '{{domain}}')">Thanh toán</button> <button type="button" class="pay-btn" onclick="openPayment(1, '{{domain}}')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
Thanh toán ngay
</button>
<div class="secure-note">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Bảo mật bởi MegaPay / VNPT EPAY
</div>
<script src="/js/jquery.min.js"></script> <script src="/js/jquery.min.js"></script>
<script src="{{domain}}/pg_was/js/payment/layer/paymentClient.js"></script> <script src="{{domain}}/pg_was/js/payment/layer/paymentClient.js"></script>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div>
</div>
</body> </body>
</html> </html>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="vi">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lịch sử giao dịch</title> <title>Lịch sử giao dịch</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 40px auto; } :root {
table { width: 100%; border-collapse: collapse; } --color-bg: #F1F5F9;
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; font-size: 14px; } --color-surface: #FFFFFF;
th { background: #f0f0f0; } --color-primary: #1E40AF;
.status-pending { color: #b8860b; } --color-primary-hover: #1D4ED8;
.status-success { color: #276b45; } --color-text: #0F172A;
.status-failed { color: #a3352a; } --color-text-muted: #64748B;
.pager { margin-top: 16px; } --color-border: #E2E8F0;
.pager a { margin-right: 12px; } --color-header-bg: #F8FAFC;
--pill-pending-bg: #FEF3C7;
--pill-pending-fg: #92400E;
--pill-success-bg: #DCFCE7;
--pill-success-fg: #166534;
--pill-failed-bg: #FEE2E2;
--pill-failed-fg: #991B1B;
--radius: 10px;
--shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.06);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--color-bg);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: var(--color-text);
padding: 40px 20px;
}
.page {
max-width: 1080px;
margin: 0 auto;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 24px;
}
h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0;
}
.subtitle {
font-size: 14px;
color: var(--color-text-muted);
margin: 4px 0 0;
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 8px;
height: 40px;
padding: 0 16px;
background: var(--color-primary);
color: #fff;
font-size: 14px;
font-weight: 600;
text-decoration: none;
border-radius: 8px;
transition: background 150ms ease;
white-space: nowrap;
}
.btn-primary:hover { background: var(--color-primary-hover); }
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
overflow: hidden;
}
.table-scroll {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13.5px;
min-width: 860px;
}
thead th {
position: sticky;
top: 0;
background: var(--color-header-bg);
text-align: left;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
padding: 12px 16px;
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
tbody td {
padding: 14px 16px;
border-bottom: 1px solid var(--color-border);
vertical-align: middle;
white-space: nowrap;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr { transition: background 120ms ease; }
tbody tr:hover { background: #F8FAFC; }
.cell-muted { color: var(--color-text-muted); }
.cell-mono {
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
}
.cell-amount {
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
font-weight: 600;
text-align: right;
}
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.pill::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.pill-pending { background: var(--pill-pending-bg); color: var(--pill-pending-fg); }
.pill-success { background: var(--pill-success-bg); color: var(--pill-success-fg); }
.pill-failed { background: var(--pill-failed-bg); color: var(--pill-failed-fg); }
.link-cell a {
color: var(--color-primary);
font-weight: 600;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
.link-cell a:hover { text-decoration: underline; }
.empty-state {
text-align: center;
padding: 64px 24px;
color: var(--color-text-muted);
}
.empty-state svg { margin-bottom: 12px; opacity: 0.5; }
.empty-state p { margin: 0; font-size: 14px; }
.pager {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
padding: 20px 0 0;
font-size: 13px;
color: var(--color-text-muted);
}
.pager a {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 7px 14px;
border: 1px solid var(--color-border);
border-radius: 8px;
color: var(--color-text);
text-decoration: none;
background: var(--color-surface);
font-weight: 500;
transition: border-color 150ms ease, background 150ms ease;
}
.pager a:hover { border-color: var(--color-primary); background: #EFF6FF; }
@media (max-width: 640px) {
body { padding: 20px 12px; }
.page-header { align-items: flex-start; }
}
</style> </style>
</head> </head>
<body> <body>
<h2>Lịch sử giao dịch</h2> <div class="page">
<p><a href="/admin/transactions/new">+ Thêm mới giao dịch</a></p> <div class="page-header">
<div>
<h1>Lịch sử giao dịch</h1>
<p class="subtitle">Danh sách giao dịch đã tạo và trạng thái thanh toán</p>
</div>
<a class="btn-primary" href="/admin/transactions/new">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Thêm mới giao dịch
</a>
</div>
<div class="card">
{% if transactions.length %}
<div class="table-scroll">
<table> <table>
<thead> <thead>
<tr> <tr>
...@@ -25,7 +239,7 @@ ...@@ -25,7 +239,7 @@
<th>Mã giao dịch</th> <th>Mã giao dịch</th>
<th>Khách hàng</th> <th>Khách hàng</th>
<th>SĐT</th> <th>SĐT</th>
<th>Số tiền</th> <th style="text-align:right">Số tiền</th>
<th>Trạng thái</th> <th>Trạng thái</th>
<th>Ghi chú</th> <th>Ghi chú</th>
<th>Link thanh toán</th> <th>Link thanh toán</th>
...@@ -34,22 +248,41 @@ ...@@ -34,22 +248,41 @@
<tbody> <tbody>
{% for tx in transactions %} {% for tx in transactions %}
<tr> <tr>
<td>{{tx.createdAt}}</td> <td class="cell-muted">{{tx.createdAt}}</td>
<td>{{tx.merTrxId}}</td> <td class="cell-mono">{{tx.merTrxId}}</td>
<td>{{tx.customerName}}</td> <td>{{tx.customerName}}</td>
<td>{{tx.customerPhone}}</td> <td class="cell-mono">{{tx.customerPhone}}</td>
<td>{{tx.amount}}</td> <td class="cell-amount">{{tx.amount}}</td>
<td class="status-{{tx.status}}">{{tx.status}}</td> <td>
<td>{{tx.resultMsg}}</td> <span class="pill pill-{{tx.status}}">
<td><a href="{{tx.paymentUrl}}" target="_blank">Link</a></td> {% if tx.status == "success" %}Thành công{% elseif tx.status == "failed" %}Thất bại{% else %}Chờ thanh toán{% endif %}
</span>
</td>
<td class="cell-muted">{{tx.resultMsg}}</td>
<td class="link-cell">
<a href="{{tx.paymentUrl}}" target="_blank" rel="noopener">
Mở link
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p class="pager"> </div>
{% else %}
<div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<p>Chưa có giao dịch nào</p>
</div>
{% endif %}
</div>
<div class="pager">
{% if hasPrev %}<a href="/admin/transactions?page={{prevPage}}">&laquo; Trang trước</a>{% endif %} {% if hasPrev %}<a href="/admin/transactions?page={{prevPage}}">&laquo; Trang trước</a>{% endif %}
Trang {{page}} <span>Trang {{page}}</span>
{% if hasNext %}<a href="/admin/transactions?page={{nextPage}}">Trang sau &raquo;</a>{% endif %} {% if hasNext %}<a href="/admin/transactions?page={{nextPage}}">Trang sau &raquo;</a>{% endif %}
</p> </div>
</div>
</body> </body>
</html> </html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment