add: gui sep requests

This commit is contained in:
2026-02-01 10:19:02 +03:30
parent f3cc7e6957
commit f7818a57c0

385
index.js
View File

@@ -1037,17 +1037,58 @@ const PROVINCE_NAMES = {
51: "کردستان",
};
// all-payments: list of saved SEP pay requests (from MongoDB)
app.get("/all-payments", async (req, res) => {
let list = [];
// all-payments/data: paginated + filtered list (JSON API)
app.get("/all-payments/data", async (req, res) => {
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const limit = Math.min(100, Math.max(5, parseInt(req.query.limit, 10) || 20));
let dateFrom = (req.query.dateFrom || "").trim(); // YYYY-MM-DD
let dateTo = (req.query.dateTo || "").trim();
if (dateFrom && !dateTo) dateTo = dateFrom;
const search = (req.query.search || "").trim();
if (dateFrom || dateTo) {
console.log("all-payments/data date filter:", { dateFrom, dateTo });
}
try {
const coll = await getSepPayCollection();
const filter = {};
// Iran timezone UTC+3:30 - treat selected date as full calendar day in Iran
const IRAN_OFFSET_MS = 3.5 * 60 * 60 * 1000;
if (dateFrom || dateTo) {
filter.createdAt = {};
if (dateFrom) {
const utcStart = new Date(dateFrom + "T00:00:00.000Z").getTime();
filter.createdAt.$gte = new Date(utcStart - IRAN_OFFSET_MS);
}
if (dateTo) {
const utcEnd = new Date(dateTo + "T23:59:59.999Z").getTime();
filter.createdAt.$lte = new Date(utcEnd - IRAN_OFFSET_MS);
}
}
if (search) {
filter.$or = [
{ amountRaw: new RegExp(escapeRegex(search), "i") },
{ phone: new RegExp(escapeRegex(search), "i") },
{ provincecode: new RegExp(escapeRegex(search), "i") },
{ resNum: new RegExp(escapeRegex(search), "i") },
];
if (!isNaN(parseInt(search, 10))) {
filter.$or.push({ amount: parseInt(search, 10) });
}
}
const total = await coll.countDocuments(filter);
const raw = await coll
.find({})
.find(filter)
.sort({ createdAt: -1 })
.limit(500)
.skip((page - 1) * limit)
.limit(limit)
.toArray();
list = raw.map((doc) => {
const list = raw.map((doc) => {
const code = (doc.provincecode || "").toString().substring(0, 2);
const provinceName = PROVINCE_NAMES[code] || doc.provincecode || "-";
return {
@@ -1056,67 +1097,173 @@ app.get("/all-payments", async (req, res) => {
provinceName,
};
});
return res.json({
list,
total,
page,
limit,
totalPages: Math.ceil(total / limit) || 1,
});
} catch (err) {
console.error("all-payments list error", err);
console.error("all-payments data error", err);
return res.status(500).json({ error: err.message });
}
});
function escapeRegex(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const listJson = JSON.stringify(list)
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029")
.replace(/</g, "\\u003c")
.replace(/>/g, "\\u003e")
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"');
// all-payments: modern UI with pagination, date filter, search
app.get("/all-payments", async (req, res) => {
const html = `<!DOCTYPE html>
<html dir="rtl" lang="fa">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>همه پرداخت‌ها</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/persian-datepicker@1.2.0/dist/css/persian-datepicker.min.css">
<style>
* { box-sizing: border-box; }
body { font-family: Tahoma, Arial, sans-serif; margin: 0; padding: 16px; background: #f5f5f5; }
h1 { color: #333; margin-bottom: 16px; }
table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden; }
th, td { padding: 10px 12px; text-align: right; border-bottom: 1px solid #eee; }
th { background: #fafafa; font-weight: bold; color: #555; }
tr:hover { background: #f9f9f9; }
.btn-send { background: #1976d2; color: #fff; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; margin-left: 6px; }
.btn-send:hover { background: #1565c0; }
.btn-send:disabled { background: #9e9e9e; cursor: not-allowed; }
.btn-remove { background: #c62828; color: #fff; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; }
.btn-remove:hover { background: #b71c1c; }
.btn-remove:disabled { background: #9e9e9e; cursor: not-allowed; }
.btn-remove-all { background: #c62828; color: #fff; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; margin-bottom: 12px; }
.btn-remove-all:hover { background: #b71c1c; }
.cell-msg { font-size: 12px; padding: 4px 0; }
.cell-msg.ok { color: #2e7d32; }
.cell-msg.err { color: #c62828; }
:root {
--bg: #0f0f14;
--surface: #18181f;
--surface-hover: #1e1e28;
--border: #2a2a36;
--text: #e4e4e7;
--text-muted: #a1a1aa;
--primary: #6366f1;
--primary-hover: #818cf8;
--danger: #ef4444;
--danger-hover: #dc2626;
--success: #22c55e;
--radius: 12px;
--radius-sm: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', -apple-system, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; padding: 24px; line-height: 1.5; }
.page { max-width: 1200px; margin: 0 auto; }
h1 { font-size: 1.75rem; font-weight: 700; margin-bottom: 24px; letter-spacing: -0.02em; }
.toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 20px; }
.filters { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; flex: 1; }
.filter-group { display: flex; align-items: center; gap: 8px; }
.filter-group label { font-size: 0.8125rem; color: var(--text-muted); font-weight: 500; }
input[type="text"], input[type="date"] {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
color: var(--text); padding: 10px 14px; font-size: 0.875rem; min-width: 140px;
}
input[type="text"]:focus, input[type="date"]:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px rgba(99,102,241,0.2); }
input[type="text"]::placeholder { color: var(--text-muted); }
.btn { border: none; border-radius: var(--radius-sm); padding: 10px 18px; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: background 0.15s, opacity 0.15s; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--primary-hover); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { background: var(--danger-hover); }
.btn-ghost { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-hover); }
.btn-sm { padding: 6px 12px; font-size: 0.8125rem; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 14px 16px; text-align: right; border-bottom: 1px solid var(--border); }
th { font-size: 0.75rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; background: rgba(0,0,0,0.2); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--surface-hover); }
td { font-size: 0.875rem; }
.cell-msg { font-size: 0.75rem; margin-top: 4px; }
.cell-msg.ok { color: var(--success); }
.cell-msg.err { color: var(--danger); }
.actions-cell { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.pagination { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; padding: 16px; border-top: 1px solid var(--border); }
.pagination-info { font-size: 0.8125rem; color: var(--text-muted); }
.pagination-btns { display: flex; gap: 6px; align-items: center; }
.pagination-btns button { min-width: 36px; }
.pagination-btns .page-num { min-width: 32px; padding: 8px; }
.empty { text-align: center; padding: 48px 24px; color: var(--text-muted); font-size: 0.9375rem; }
.loading { text-align: center; padding: 48px; color: var(--text-muted); }
.pwt-btn-today { font-family: inherit !important; }
.datepicker-input { cursor: pointer; }
</style>
</head>
<body>
<div class="page">
<h1>همه پرداخت‌ها</h1>
<div id="toolbar"></div>
<div id="list"></div>
<div class="toolbar">
<div class="filters">
<div class="filter-group">
<label>از تاریخ</label>
<input type="text" id="dateFrom" class="datepicker-input" readonly placeholder="انتخاب تاریخ" autocomplete="off" />
<input type="hidden" id="dateFromGregorian" />
</div>
<div class="filter-group">
<label>تا تاریخ</label>
<input type="text" id="dateTo" class="datepicker-input" readonly placeholder="انتخاب تاریخ" autocomplete="off" />
<input type="hidden" id="dateToGregorian" />
</div>
<div class="filter-group">
<input type="text" id="search" placeholder="جستجو (مبلغ، موبایل، استان...) " />
</div>
<button type="button" class="btn btn-ghost" id="btn-apply">اعمال فیلتر</button>
</div>
<button type="button" class="btn btn-danger" id="btn-remove-all">حذف همه</button>
</div>
<div class="card">
<div class="table-wrap">
<div id="table-content"></div>
</div>
<div id="pagination" class="pagination" style="display:none;"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/persian-date@1.1.0/dist/persian-date.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/persian-datepicker@1.2.0/dist/js/persian-datepicker.min.js"></script>
<script>
(function() {
var list = JSON.parse("${listJson}");
var listEl = document.getElementById('list');
var toolbarEl = document.getElementById('toolbar');
if (!list || list.length === 0) {
listEl.innerHTML = '<p>موردی یافت نشد.</p>';
return;
var state = { page: 1, limit: 20, dateFrom: '', dateTo: '', search: '' };
var contentEl = document.getElementById('table-content');
var paginationEl = document.getElementById('pagination');
function persianDigitsToAscii(s) {
var p = '۰۱۲۳۴۵۶۷۸۹';
var a = '0123456789';
var out = '';
for (var i = 0; i < s.length; i++) {
var idx = p.indexOf(s[i]);
out += idx !== -1 ? a[idx] : s[i];
}
toolbarEl.innerHTML = '<button type="button" class="btn-remove-all" id="btn-remove-all">حذف همه</button>';
var rows = list.map(function(item) {
return out;
}
function persianToGregorianStr(pStr) {
if (!pStr || typeof pStr !== 'string' || !pStr.trim()) return '';
if (typeof persianDate === 'undefined') return '';
try {
var normalized = persianDigitsToAscii(pStr.trim());
var d = new persianDate(normalized).toDate();
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
} catch (e) { return ''; }
}
function buildQuery() {
var q = 'page=' + state.page + '&limit=' + state.limit;
if (state.dateFrom) q += '&dateFrom=' + encodeURIComponent(state.dateFrom);
if (state.dateTo) q += '&dateTo=' + encodeURIComponent(state.dateTo);
if (state.search) q += '&search=' + encodeURIComponent(state.search);
return q;
}
function renderRow(item) {
var createdAt = item.createdAt ? new Date(item.createdAt).toLocaleString('fa-IR') : '-';
var id = item._id;
return '<tr><td>' + (item.amountRaw || item.amount) + '</td><td>' + (item.provinceName || '-') + '</td><td>' + (item.isLink ? 'بله' : 'خیر') + '</td><td>' + (item.phone || '-') + '</td><td>' + createdAt + '</td><td><button type="button" class="btn-send" data-id="' + id + '">ارسال به سرور</button><button type="button" class="btn-remove" data-id="' + id + '">حذف</button><div class="cell-msg" id="msg-' + id + '"></div></td></tr>';
}).join('');
listEl.innerHTML = '<table><thead><tr><th>مبلغ</th><th>استان</th><th>لینک</th><th>موبایل</th><th>تاریخ</th><th>عملیات</th></tr></thead><tbody>' + rows + '</tbody></table>';
listEl.querySelectorAll('.btn-send').forEach(function(btn) {
btn.addEventListener('click', function() {
return '<tr data-id="' + id + '"><td>' + (item.amountRaw || item.amount) + '</td><td>' + (item.provinceName || '-') + '</td><td>' + (item.isLink ? 'بله' : 'خیر') + '</td><td>' + (item.phone || '-') + '</td><td>' + createdAt + '</td><td><div class="actions-cell"><button type="button" class="btn btn-primary btn-sm btn-send" data-id="' + id + '">ارسال به سرور</button><button type="button" class="btn btn-danger btn-sm btn-remove" data-id="' + id + '">حذف</button><div class="cell-msg" id="msg-' + id + '"></div></div></td></tr>';
}
function bindRowEvents(fragment) {
if (!fragment || !fragment.querySelectorAll) return;
fragment.querySelectorAll('.btn-send').forEach(function(btn) {
btn.onclick = function() {
if (!confirm('آیا مطمئن هستید؟')) return;
var id = btn.getAttribute('data-id');
var msgEl = document.getElementById('msg-' + id);
@@ -1125,21 +1272,15 @@ app.get("/all-payments", async (req, res) => {
fetch('/all-payments/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) })
.then(function(r) { return r.json(); })
.then(function(j) {
if (j.error) {
if (msgEl) { msgEl.textContent = j.error; msgEl.className = 'cell-msg err'; }
} else {
var row = btn.closest('tr');
if (row) row.remove();
}
})
.catch(function(e) {
if (msgEl) { msgEl.textContent = e.message; msgEl.className = 'cell-msg err'; }
if (j.error && msgEl) { msgEl.textContent = j.error; msgEl.className = 'cell-msg err'; }
else { var row = document.querySelector('tr[data-id="' + id + '"]'); if (row) { row.remove(); if (!contentEl.querySelector('tbody tr')) load(); } }
})
.catch(function(e) { if (msgEl) msgEl.textContent = e.message; msgEl.className = 'cell-msg err'; })
.finally(function() { btn.disabled = false; });
};
});
});
listEl.querySelectorAll('.btn-remove').forEach(function(btn) {
btn.addEventListener('click', function() {
fragment.querySelectorAll('.btn-remove').forEach(function(btn) {
btn.onclick = function() {
if (!confirm('آیا از حذف این مورد مطمئن هستید؟')) return;
var id = btn.getAttribute('data-id');
var msgEl = document.getElementById('msg-' + id);
@@ -1149,25 +1290,137 @@ app.get("/all-payments", async (req, res) => {
.then(function(r) { return r.json(); })
.then(function(j) {
if (j.error && msgEl) { msgEl.textContent = j.error; msgEl.className = 'cell-msg err'; }
else { var row = btn.closest('tr'); if (row) row.remove(); }
else { var row = document.querySelector('tr[data-id="' + id + '"]'); if (row) { row.remove(); if (!contentEl.querySelector('tbody tr')) load(); } }
})
.catch(function(e) { if (msgEl) { msgEl.textContent = e.message; msgEl.className = 'cell-msg err'; } })
.catch(function(e) { if (msgEl) msgEl.textContent = e.message; msgEl.className = 'cell-msg err'; })
.finally(function() { btn.disabled = false; });
};
});
}
function renderPagination(data) {
var totalPages = data.totalPages;
var page = data.page;
var total = data.total;
var start = (page - 1) * data.limit + 1;
var end = Math.min(page * data.limit, total);
if (total === 0) { paginationEl.style.display = 'none'; return; }
paginationEl.style.display = 'flex';
var info = 'نمایش ' + start + ' تا ' + end + ' از ' + total + ' مورد';
var btns = '';
btns += '<button type="button" class="btn btn-ghost btn-sm" data-page="' + (page - 1) + '" ' + (page <= 1 ? 'disabled' : '') + '>قبلی</button>';
var from = Math.max(1, page - 2);
var to = Math.min(totalPages, page + 2);
for (var i = from; i <= to; i++) {
btns += '<button type="button" class="btn btn-sm page-num ' + (i === page ? 'btn-primary' : 'btn-ghost') + '" data-page="' + i + '">' + i + '</button>';
}
btns += '<button type="button" class="btn btn-ghost btn-sm" data-page="' + (page + 1) + '" ' + (page >= totalPages ? 'disabled' : '') + '>بعدی</button>';
paginationEl.innerHTML = '<span class="pagination-info">' + info + '</span><div class="pagination-btns">' + btns + '</div>';
paginationEl.querySelectorAll('[data-page]').forEach(function(b) {
b.onclick = function() {
var p = parseInt(b.getAttribute('data-page'), 10);
if (p >= 1 && p <= totalPages) { state.page = p; load(); }
};
});
document.getElementById('btn-remove-all').addEventListener('click', function() {
}
function load() {
contentEl.innerHTML = '<div class="loading">در حال بارگذاری...</div>';
fetch('/all-payments/data?' + buildQuery())
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { contentEl.innerHTML = '<div class="empty">' + data.error + '</div>'; return; }
var list = data.list || [];
if (list.length === 0) {
contentEl.innerHTML = '<div class="empty">موردی یافت نشد.</div>';
paginationEl.style.display = 'none';
return;
}
var thead = '<table><thead><tr><th>مبلغ</th><th>استان</th><th>لینک</th><th>موبایل</th><th>تاریخ</th><th>عملیات</th></tr></thead><tbody>';
var rows = list.map(renderRow).join('');
contentEl.innerHTML = thead + rows + '</tbody></table>';
bindRowEvents(contentEl);
renderPagination(data);
})
.catch(function(e) {
contentEl.innerHTML = '<div class="empty">خطا: ' + e.message + '</div>';
});
}
function unixToGregorianYMD(unix) {
var d = new Date(unix);
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
}
function initDatepickers() {
if (typeof jQuery === 'undefined' || !jQuery.fn.pDatepicker) return;
jQuery('#dateFrom').pDatepicker({
format: 'YYYY/MM/DD',
initialValue: false,
observer: true,
calendar: { persian: { locale: 'fa' } },
autoClose: true,
onSelect: function(unix) {
if (unix) {
var g = unixToGregorianYMD(unix);
document.getElementById('dateFromGregorian').value = g;
if (typeof persianDate !== 'undefined') {
var pd = new persianDate(unix);
document.getElementById('dateFrom').value = pd.format('YYYY/MM/DD');
}
}
}
});
jQuery('#dateTo').pDatepicker({
format: 'YYYY/MM/DD',
initialValue: false,
observer: true,
calendar: { persian: { locale: 'fa' } },
autoClose: true,
onSelect: function(unix) {
if (unix) {
var g = unixToGregorianYMD(unix);
document.getElementById('dateToGregorian').value = g;
if (typeof persianDate !== 'undefined') {
var pd = new persianDate(unix);
document.getElementById('dateTo').value = pd.format('YYYY/MM/DD');
}
}
}
});
}
document.getElementById('btn-apply').onclick = function() {
document.getElementById('dateFrom').blur();
document.getElementById('dateTo').blur();
state.dateFrom = (document.getElementById('dateFromGregorian').value || '').trim();
state.dateTo = (document.getElementById('dateToGregorian').value || '').trim();
if (state.dateFrom && !state.dateTo) state.dateTo = state.dateFrom;
state.search = document.getElementById('search').value.trim();
state.page = 1;
load();
};
document.getElementById('search').onkeydown = function(e) { if (e.key === 'Enter') document.getElementById('btn-apply').click(); };
document.getElementById('btn-remove-all').onclick = function() {
if (!confirm('آیا از حذف همه موارد مطمئن هستید؟')) return;
var btn = this;
btn.disabled = true;
fetch('/all-payments/remove-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
.then(function(r) { return r.json(); })
.then(function(j) {
if (j.error) { alert(j.error); }
else { listEl.innerHTML = '<p>موردی یافت نشد.</p>'; toolbarEl.innerHTML = ''; }
if (j.error) alert(j.error);
else { state.page = 1; load(); }
})
.catch(function(e) { alert(e.message); })
.finally(function() { btn.disabled = false; });
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { initDatepickers(); load(); });
} else {
initDatepickers();
load();
}
})();
</script>
</body>