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>
<html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
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>
</head>
<body>
{% if success %}
<h2 style="color:#276b45">Thanh toán thành công</h2>
{% else %}
<h2 style="color:#a3352a">Thanh toán thất bại</h2>
<p>{{message}}</p>
{% endif %}
<div class="card">
{% if success %}
<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 %}
<div class="state-icon failed">
<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 %}
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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 %}
<link rel="stylesheet" href="{{domain}}/pg_was/css/payment/layer/paymentClient.css">
{% 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>
<body>
{% if notFound %}
<h2>Không tìm thấy giao dịch</h2>
<p>Đường dẫn thanh toán không hợp lệ hoặc đã hết hạn.</p>
{% else %}
{% if alreadyPaid %}
<h2>Giao dịch này đã được xử lý</h2>
<p>Trạng thái: {{tx.status}}</p>
{% else %}
<h2>Thanh toán {{tx.amount}} VNĐ</h2>
<p>Khách hàng: {{tx.customerName}}</p>
<form id="megapayForm" name="megapayForm" method="POST">
<input type="hidden" name="merId" value="{{merId}}">
<input type="hidden" name="currency" value="VND">
<input type="hidden" name="amount" value="{{tx.amount}}">
<input type="hidden" name="invoiceNo" value="{{tx.transCode}}">
<input type="hidden" name="merTrxId" value="{{tx.merTrxId}}">
<input type="hidden" name="goodsNm" value="Thanh toan Hai Yen">
<input type="hidden" name="payType" value="{{tx.payType}}">
<input type="hidden" name="description" value="Thanh toan don hang">
<input type="hidden" name="callBackUrl" value="{{callBackUrl}}">
<input type="hidden" name="notiUrl" value="{{notiUrl}}">
<input type="hidden" name="reqDomain" value="{{reqDomain}}">
<input type="hidden" name="merchantToken" value="{{tx.merchantToken}}">
<input type="hidden" name="timeStamp" value="{{tx.timeStamp}}">
<input type="hidden" name="userLanguage" value="VN">
<input type="hidden" name="windowColor" value="#ef5459">
<input type="hidden" name="windowType" value="">
</form>
<button onclick="openPayment(1, '{{domain}}')">Thanh toán</button>
<script src="/js/jquery.min.js"></script>
<script src="{{domain}}/pg_was/js/payment/layer/paymentClient.js"></script>
{% endif %}
{% endif %}
<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 %}
<div class="state-icon neutral">
<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 %}
{% if alreadyPaid %}
<div class="state-icon success">
<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 %}
<div class="amount-display">
<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">
<input type="hidden" name="merId" value="{{merId}}">
<input type="hidden" name="currency" value="VND">
<input type="hidden" name="amount" value="{{tx.amount}}">
<input type="hidden" name="invoiceNo" value="{{tx.transCode}}">
<input type="hidden" name="merTrxId" value="{{tx.merTrxId}}">
<input type="hidden" name="goodsNm" value="Thanh toan Hai Yen">
<input type="hidden" name="payType" value="{{tx.payType}}">
<input type="hidden" name="description" value="Thanh toan don hang">
<input type="hidden" name="callBackUrl" value="{{callBackUrl}}">
<input type="hidden" name="notiUrl" value="{{notiUrl}}">
<input type="hidden" name="reqDomain" value="{{reqDomain}}">
<input type="hidden" name="merchantToken" value="{{tx.merchantToken}}">
<input type="hidden" name="timeStamp" value="{{tx.timeStamp}}">
<input type="hidden" name="userLanguage" value="VN">
<input type="hidden" name="windowColor" value="#ef5459">
<input type="hidden" name="windowType" value="">
</form>
<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="{{domain}}/pg_was/js/payment/layer/paymentClient.js"></script>
{% endif %}
{% endif %}
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 40px auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; font-size: 14px; }
th { background: #f0f0f0; }
.status-pending { color: #b8860b; }
.status-success { color: #276b45; }
.status-failed { color: #a3352a; }
.pager { margin-top: 16px; }
.pager a { margin-right: 12px; }
:root {
--color-bg: #F1F5F9;
--color-surface: #FFFFFF;
--color-primary: #1E40AF;
--color-primary-hover: #1D4ED8;
--color-text: #0F172A;
--color-text-muted: #64748B;
--color-border: #E2E8F0;
--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>
</head>
<body>
<h2>Lịch sử giao dịch</h2>
<p><a href="/admin/transactions/new">+ Thêm mới giao dịch</a></p>
<table>
<thead>
<tr>
<th>Thời gian</th>
<th>Mã giao dịch</th>
<th>Khách hàng</th>
<th>SĐT</th>
<th>Số tiền</th>
<th>Trạng thái</th>
<th>Ghi chú</th>
<th>Link thanh toán</th>
</tr>
</thead>
<tbody>
{% for tx in transactions %}
<tr>
<td>{{tx.createdAt}}</td>
<td>{{tx.merTrxId}}</td>
<td>{{tx.customerName}}</td>
<td>{{tx.customerPhone}}</td>
<td>{{tx.amount}}</td>
<td class="status-{{tx.status}}">{{tx.status}}</td>
<td>{{tx.resultMsg}}</td>
<td><a href="{{tx.paymentUrl}}" target="_blank">Link</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="pager">
{% if hasPrev %}<a href="/admin/transactions?page={{prevPage}}">&laquo; Trang trước</a>{% endif %}
Trang {{page}}
{% if hasNext %}<a href="/admin/transactions?page={{nextPage}}">Trang sau &raquo;</a>{% endif %}
</p>
<div class="page">
<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>
<thead>
<tr>
<th>Thời gian</th>
<th>Mã giao dịch</th>
<th>Khách hàng</th>
<th>SĐT</th>
<th style="text-align:right">Số tiền</th>
<th>Trạng thái</th>
<th>Ghi chú</th>
<th>Link thanh toán</th>
</tr>
</thead>
<tbody>
{% for tx in transactions %}
<tr>
<td class="cell-muted">{{tx.createdAt}}</td>
<td class="cell-mono">{{tx.merTrxId}}</td>
<td>{{tx.customerName}}</td>
<td class="cell-mono">{{tx.customerPhone}}</td>
<td class="cell-amount">{{tx.amount}}</td>
<td>
<span class="pill pill-{{tx.status}}">
{% 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>
{% endfor %}
</tbody>
</table>
</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 %}
<span>Trang {{page}}</span>
{% if hasNext %}<a href="/admin/transactions?page={{nextPage}}">Trang sau &raquo;</a>{% endif %}
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Thêm mới 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>
body { font-family: Arial, sans-serif; max-width: 480px; margin: 40px auto; }
label { display: block; margin-top: 12px; font-weight: bold; }
input { width: 100%; padding: 8px; margin-top: 4px; box-sizing: border-box; }
button { margin-top: 20px; padding: 10px 20px; background: #2d6cdf; color: #fff; border: none; cursor: pointer; }
#result { margin-top: 20px; padding: 12px; background: #f0f0f0; word-break: break-all; display: none; }
:root {
--color-bg: #F1F5F9;
--color-surface: #FFFFFF;
--color-primary: #1E40AF;
--color-primary-hover: #1D4ED8;
--color-text: #0F172A;
--color-text-muted: #64748B;
--color-border: #E2E8F0;
--color-danger: #DC2626;
--color-danger-bg: #FEF2F2;
--color-danger-border: #FECACA;
--color-success: #15803D;
--color-success-bg: #F0FDF4;
--color-success-border: #BBF7D0;
--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);
display: flex;
justify-content: center;
padding: 40px 20px;
}
.page {
width: 100%;
max-width: 480px;
}
.breadcrumb {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-muted);
text-decoration: none;
margin-bottom: 20px;
transition: color 150ms ease;
}
.breadcrumb:hover { color: var(--color-primary); }
.breadcrumb svg { flex-shrink: 0; }
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-card);
padding: 32px;
}
h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 4px;
}
.subtitle {
font-size: 14px;
color: var(--color-text-muted);
margin: 0 0 28px;
}
.field { margin-bottom: 20px; }
label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 6px;
}
.field-hint {
font-size: 12px;
color: var(--color-text-muted);
margin-top: 6px;
}
input {
width: 100%;
height: 44px;
padding: 0 14px;
font-size: 15px;
font-family: inherit;
color: var(--color-text);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
outline: none;
transition: border-color 150ms ease, box-shadow 150ms ease;
}
input::placeholder { color: #A0AEC0; }
input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.12);
}
.amount-field { position: relative; }
.amount-field input { padding-right: 52px; }
.amount-suffix {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 13px;
font-weight: 600;
color: var(--color-text-muted);
pointer-events: none;
}
button[type="submit"] {
width: 100%;
height: 46px;
margin-top: 8px;
background: var(--color-primary);
color: #fff;
font-family: inherit;
font-size: 15px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background 150ms ease, transform 150ms ease;
}
button[type="submit"]:hover { background: var(--color-primary-hover); }
button[type="submit"]:active { transform: scale(0.99); }
button[type="submit"]:disabled { opacity: 0.7; cursor: not-allowed; }
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.4);
border-top-color: #fff;
border-radius: 50%;
animation: spin 700ms linear infinite;
display: none;
}
button[type="submit"].loading .spinner { display: inline-block; }
button[type="submit"].loading .btn-label::after { content: "Đang tạo…"; }
button[type="submit"].loading .btn-label { font-size: 0; }
button[type="submit"].loading .btn-label::after { font-size: 15px; }
@keyframes spin { to { transform: rotate(360deg); } }
#result { margin-top: 20px; }
.alert {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 16px;
border-radius: 8px;
border: 1px solid;
font-size: 14px;
line-height: 1.5;
}
.alert svg { flex-shrink: 0; margin-top: 1px; }
.alert-success {
background: var(--color-success-bg);
border-color: var(--color-success-border);
color: #14532D;
}
.alert-error {
background: var(--color-danger-bg);
border-color: var(--color-danger-border);
color: #7F1D1D;
}
.alert-title { font-weight: 600; margin: 0 0 6px; }
.link-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
background: #fff;
border: 1px solid var(--color-success-border);
border-radius: 6px;
padding: 8px 10px;
}
.link-row input {
flex: 1;
height: auto;
border: none;
padding: 0;
font-size: 13px;
color: var(--color-text);
background: transparent;
}
.link-row input:focus { box-shadow: none; }
.copy-btn {
flex-shrink: 0;
height: 30px;
padding: 0 12px;
font-family: inherit;
font-size: 12px;
font-weight: 600;
color: var(--color-success);
background: #fff;
border: 1px solid var(--color-success-border);
border-radius: 6px;
cursor: pointer;
transition: background 150ms ease;
}
.copy-btn:hover { background: var(--color-success-bg); }
@media (prefers-reduced-motion: reduce) {
.spinner { animation: none; }
* { transition: none !important; }
}
</style>
</head>
<body>
<h2>Thêm mới giao dịch</h2>
<p><a href="/admin/transactions">Xem lịch sử giao dịch</a></p>
<form id="txForm">
<label>Tên khách hàng</label>
<input type="text" name="customerName" required>
<label>Số điện thoại</label>
<input type="text" name="customerPhone" required>
<label>Địa chỉ</label>
<input type="text" name="customerAddress" required>
<label>Số tiền thanh toán (VNĐ)</label>
<input type="number" name="amount" required min="1000">
<button type="submit">Tạo giao dịch</button>
</form>
<div id="result"></div>
<div class="page">
<a class="breadcrumb" href="/admin/transactions">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Lịch sử giao dịch
</a>
<div class="card">
<h1>Thêm mới giao dịch</h1>
<p class="subtitle">Nhập thông tin khách hàng để tạo link thanh toán</p>
<form id="txForm" novalidate>
<div class="field">
<label for="customerName">Tên khách hàng</label>
<input type="text" id="customerName" name="customerName" placeholder="Nguyễn Văn A" autocomplete="name" required>
</div>
<div class="field">
<label for="customerPhone">Số điện thoại</label>
<input type="tel" id="customerPhone" name="customerPhone" placeholder="09xx xxx xxx" autocomplete="tel" required>
</div>
<div class="field">
<label for="customerAddress">Địa chỉ</label>
<input type="text" id="customerAddress" name="customerAddress" placeholder="Số nhà, đường, phường/xã" autocomplete="street-address" required>
</div>
<div class="field">
<label for="amount">Số tiền thanh toán</label>
<div class="amount-field">
<input type="number" id="amount" name="amount" placeholder="0" min="1000" step="1000" inputmode="numeric" required>
<span class="amount-suffix">VNĐ</span>
</div>
</div>
<button type="submit" id="submitBtn">
<span class="spinner"></span>
<span class="btn-label">Tạo giao dịch</span>
</button>
</form>
<div id="result"></div>
</div>
</div>
<script>
var iconCheck = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9 12l2 2 4-4"/></svg>';
var iconAlert = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
document.getElementById('txForm').addEventListener('submit', function (e) {
e.preventDefault();
var form = e.target;
var btn = document.getElementById('submitBtn');
var data = {
customerName: form.customerName.value,
customerPhone: form.customerPhone.value,
customerAddress: form.customerAddress.value,
amount: form.amount.value
};
btn.classList.add('loading');
btn.disabled = true;
fetch('/admin/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
......@@ -44,12 +314,37 @@
.then(function (r) { return r.json(); })
.then(function (res) {
var el = document.getElementById('result');
el.style.display = 'block';
if (res.code === '00') {
el.innerHTML = 'Link thanh toán: <a href="' + res.data.paymentUrl + '" target="_blank">' + res.data.paymentUrl + '</a>';
el.innerHTML =
'<div class="alert alert-success">' + iconCheck +
'<div><p class="alert-title">Đã tạo giao dịch thành công</p>' +
'<div class="link-row">' +
'<input type="text" readonly value="' + res.data.paymentUrl + '" id="paymentUrlField">' +
'<button type="button" class="copy-btn" id="copyBtn">Sao chép</button>' +
'</div></div></div>';
document.getElementById('copyBtn').addEventListener('click', function () {
var field = document.getElementById('paymentUrlField');
field.select();
navigator.clipboard && navigator.clipboard.writeText(field.value);
this.textContent = 'Đã chép!';
var self = this;
setTimeout(function () { self.textContent = 'Sao chép'; }, 1500);
});
form.reset();
} else {
el.innerHTML = 'Lỗi: ' + res.data;
el.innerHTML =
'<div class="alert alert-error">' + iconAlert +
'<div><p class="alert-title">Không tạo được giao dịch</p>' + res.data + '</div></div>';
}
})
.catch(function () {
document.getElementById('result').innerHTML =
'<div class="alert alert-error">' + iconAlert +
'<div><p class="alert-title">Lỗi kết nối</p>Vui lòng thử lại.</div></div>';
})
.finally(function () {
btn.classList.remove('loading');
btn.disabled = false;
});
});
</script>
......
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