- 迁移链追加 0009_reminders_engine:旧 manual reminders 表改名 reminders_legacy_manual 保留历史,新建 reminders/reminder_events/ reminder_settings(append-only 触发器) - server.py:新提醒 API(pending/scan/send/manual/resend/settings/ 公司端收件与状态流转)替换旧 manual-send 端点 - web:admin 待提醒清单+扫描+详情抽屉,公司端通知动态化+ 去处理事件委托;设置保存同步提醒扫描参数 - 移除被取代的 settings.pending_items/send_reminders 与旧 UI 逻辑 - 全量测试 346 项通过(5 项浏览器跳过与历史一致) Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+59
-10
@@ -633,6 +633,22 @@
|
||||
<h1>提醒管理</h1>
|
||||
<p class="page-sub">向成员公司发送处理提醒,并跟踪系统提醒与人工提醒的触达与处理状态。</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<button type="button" class="btn btn-sm" id="reminder-scan-btn">立即扫描</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="pending-reminders-card" style="margin-bottom: 16px;">
|
||||
<div class="card-head">
|
||||
<span class="card-title">待提醒清单<span class="sub">系统按流水提交、断档、待确认自动发现,点发送即送达对应公司</span></span>
|
||||
<div class="row" style="gap: 10px; align-items: center;">
|
||||
<span class="meta" id="pending-summary">—</span>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="pending-send-all" disabled>全部一键发送</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pending-list">
|
||||
<div class="empty" id="pending-empty">暂无需要提醒的事项 · 各公司流水与确认进度正常</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-1-2">
|
||||
@@ -642,18 +658,30 @@
|
||||
</div>
|
||||
<form id="reminderForm" novalidate>
|
||||
<div class="field" style="margin-bottom: 14px;">
|
||||
<label for="reminder-company">接收公司</label>
|
||||
<select class="select" id="reminder-company"><option value="">请选择公司</option></select>
|
||||
<span class="hint error" id="company-error" style="display: none;">请选择一家接收公司</span>
|
||||
<label>接收公司(可多选)</label>
|
||||
<div class="row" id="reminderCompanyList" style="flex-wrap: wrap; gap: 8px 16px;"></div>
|
||||
<span class="hint error" id="company-error" style="display: none;">请至少选择一家接收公司</span>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 16px;">
|
||||
<label>待提醒事项(系统自动列出)</label>
|
||||
<div id="reminder-items">
|
||||
<div class="empty">请先选择公司,系统将自动列出该公司待提醒事项。</div>
|
||||
</div>
|
||||
<span class="hint error" id="reminder-error" style="display: none;">该公司当前没有待提醒事项</span>
|
||||
<div class="field" style="margin-bottom: 14px;">
|
||||
<label for="reminder-type">提醒类型</label>
|
||||
<select class="select" id="reminder-type">
|
||||
<option selected>流水未提交</option>
|
||||
<option>单边待确认</option>
|
||||
<option>科目待确认</option>
|
||||
<option>账户登记</option>
|
||||
<option>其他</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="send-btn" style="width: 100%;" disabled>发送提醒</button>
|
||||
<div class="field" style="margin-bottom: 14px;">
|
||||
<label for="reminder-content">提醒内容</label>
|
||||
<textarea class="textarea" id="reminder-content">请于截止日期前完成 2026 年 7 月银行流水上传与待确认事项处理。</textarea>
|
||||
<span class="hint error" id="content-error" style="display: none;">提醒内容不能为空</span>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 18px;">
|
||||
<label for="reminder-deadline">截止日期</label>
|
||||
<input class="input" type="date" id="reminder-deadline" value="2026-08-29" min="2026-08-20" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="send-btn" style="width: 100%;">发送提醒</button>
|
||||
<p class="hint" id="send-hint" style="margin-top: 10px; display: none;"></p>
|
||||
</form>
|
||||
</div>
|
||||
@@ -672,6 +700,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>公司</th>
|
||||
<th>来源</th>
|
||||
<th>类型</th>
|
||||
<th class="wrap">内容摘要</th>
|
||||
<th>发送时间</th>
|
||||
@@ -947,6 +976,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提醒详情抽屉 -->
|
||||
<aside class="drawer" id="reminder-detail-drawer" aria-label="提醒详情">
|
||||
<div class="drawer-head">
|
||||
<div>
|
||||
<span class="pill pill-info" id="rd-source-pill">系统</span>
|
||||
<h2 class="d-title" id="rd-title">提醒详情</h2>
|
||||
<p class="d-desc" id="rd-sub"></p>
|
||||
</div>
|
||||
<button type="button" class="icon-button" data-close-rd aria-label="关闭详情" title="关闭详情">×</button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
<dl class="kv" id="rd-fields"></dl>
|
||||
<h3 style="font-size: 13px; margin: 16px 0 8px; color: var(--muted);">事件留痕</h3>
|
||||
<div id="rd-events"></div>
|
||||
</div>
|
||||
<div class="drawer-foot">
|
||||
<button type="button" class="btn" data-close-rd>关闭</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 提醒历史弹窗 -->
|
||||
<div class="modal-backdrop" id="history-modal">
|
||||
<div class="modal">
|
||||
|
||||
+414
-211
@@ -465,7 +465,12 @@ function initShell() {
|
||||
}
|
||||
});
|
||||
$$("[data-view]").forEach((button) => button.addEventListener("click", (event) => { event.preventDefault(); showView(button.dataset.view); }));
|
||||
$$("[data-view-link]").forEach((button) => button.addEventListener("click", (event) => { event.preventDefault(); showView(button.dataset.viewLink); }));
|
||||
document.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("[data-view-link]");
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
showView(button.dataset.viewLink);
|
||||
});
|
||||
$$(".logout").forEach((control) => control.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
@@ -1122,7 +1127,7 @@ function setSelectOptions(select, names, { keepFirst = false } = {}) {
|
||||
select.replaceChildren(...kept, ...names.map((name) => new Option(name, name)));
|
||||
}
|
||||
|
||||
function fillCompanySelects(names) {
|
||||
function fillCompanySelects(names, companies) {
|
||||
// Every company picker is driven by master data: a newly created company
|
||||
// appears in pair queries, audit filters, flow filters and reminders
|
||||
// without any code change.
|
||||
@@ -1134,11 +1139,20 @@ function fillCompanySelects(names) {
|
||||
});
|
||||
setSelectOptions($("#auditCompany"), names, { keepFirst: true });
|
||||
setSelectOptions($("#flowCompany"), names, { keepFirst: true });
|
||||
const reminderSelect = $("#reminder-company");
|
||||
if (reminderSelect) {
|
||||
const kept = reminderSelect.querySelector('option[value=""]') ? [reminderSelect.options[0].cloneNode(true)] : [];
|
||||
const companies = state.companies || [];
|
||||
reminderSelect.replaceChildren(...kept, ...companies.map((company) => new Option(company.name, company.id)));
|
||||
const reminderList = $("#reminderCompanyList");
|
||||
if (reminderList && companies?.length) {
|
||||
reminderList.replaceChildren(...companies.map((company) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "row";
|
||||
label.style.cssText = "gap:6px;font-size:13px;";
|
||||
const input = document.createElement("input");
|
||||
input.type = "checkbox";
|
||||
input.name = "company";
|
||||
input.value = String(company.id);
|
||||
input.style.width = "auto";
|
||||
label.append(input, document.createTextNode(company.name));
|
||||
return label;
|
||||
}));
|
||||
}
|
||||
setSelectOptions($('#openingDialog [name="from"]'), names);
|
||||
setSelectOptions($('#openingDialog [name="to"]'), names);
|
||||
@@ -1775,6 +1789,293 @@ function initDashboard() {
|
||||
loadAdminDashboard();
|
||||
}
|
||||
|
||||
function formatReminderTime(value) {
|
||||
if (!value) return "—";
|
||||
const text = String(value).replace("T", " ").slice(0, 16);
|
||||
return text;
|
||||
}
|
||||
|
||||
function reminderTypePill(ruleKey) {
|
||||
if (ruleKey === "pending_review") return "pill-warn";
|
||||
if (ruleKey === "manual") return "pill-warn";
|
||||
return "pill-danger";
|
||||
}
|
||||
|
||||
function reminderStatusPill(statusUi) {
|
||||
return { unread: "pill-danger", doing: "pill-warn", done: "pill-success" }[statusUi] || "pill-muted";
|
||||
}
|
||||
|
||||
function reminderStatusLabel(statusUi) {
|
||||
return { unread: "未读", doing: "处理中", done: "已完成" }[statusUi] || statusUi;
|
||||
}
|
||||
|
||||
async function loadAdminRemindersPending() {
|
||||
const list = $("#pending-list");
|
||||
const empty = $("#pending-empty");
|
||||
const summary = $("#pending-summary");
|
||||
const sendAll = $("#pending-send-all");
|
||||
if (!list) return;
|
||||
const response = await fetch("/api/admin/reminders/pending").catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response?.ok) return;
|
||||
const findings = result.findings || [];
|
||||
list.querySelectorAll(".list-row").forEach((row) => row.remove());
|
||||
if (!findings.length) {
|
||||
if (empty) empty.style.display = "";
|
||||
if (summary) summary.textContent = "0 家公司 · 0 项";
|
||||
if (sendAll) { sendAll.disabled = true; sendAll.textContent = "全部一键发送"; }
|
||||
return;
|
||||
}
|
||||
if (empty) empty.style.display = "none";
|
||||
const companies = new Set(findings.map((item) => item.company_id)).size;
|
||||
if (summary) summary.textContent = `${companies} 家公司 · ${findings.length} 项`;
|
||||
if (sendAll) {
|
||||
sendAll.disabled = false;
|
||||
sendAll.textContent = "全部一键发送";
|
||||
sendAll.dataset.keys = JSON.stringify(findings.map((item) => item.dedupe_key));
|
||||
}
|
||||
findings.forEach((item) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "list-row";
|
||||
row.dataset.dedupeKey = item.dedupe_key;
|
||||
const sentHint = item.send_count > 0 ? ` · 已提醒 ${item.send_count} 次` : "";
|
||||
row.innerHTML =
|
||||
'<label class="row" style="gap:6px;flex:none;"><input type="checkbox" class="pending-check" style="width:auto;" /></label>' +
|
||||
`<span class="pill ${reminderTypePill(item.rule_key)}">${item.rule_label}</span>` +
|
||||
`<div class="lr-main"><div class="lr-title">${item.title}</div>` +
|
||||
`<div class="lr-sub"><span class="meta">${item.reason}${sentHint}</span></div></div>` +
|
||||
`<span class="pill ${item.duration_pill}" style="flex:none;font-family:var(--font-mono);">${item.days_open} 天</span>` +
|
||||
`<div class="lr-side"><button type="button" class="btn btn-sm btn-primary pending-send-one">发送提醒</button></div>`;
|
||||
list.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAdminRemindersHistory(sourceFilter) {
|
||||
const tbody = $("#reminder-tbody");
|
||||
if (!tbody) return;
|
||||
const query = sourceFilter && sourceFilter !== "all" ? `?source=${sourceFilter === "system" ? "auto" : "manual"}` : "";
|
||||
const response = await fetch(`/api/admin/reminders${query}`).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response?.ok) return;
|
||||
const items = result.reminders || [];
|
||||
tbody.replaceChildren(...items.map((item) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.source = item.source === "auto" ? "system" : "manual";
|
||||
tr.dataset.reminderId = String(item.id);
|
||||
const summary = item.content.length > 46 ? item.content.slice(0, 46) + "…" : item.content;
|
||||
const sourceCell = item.source === "auto"
|
||||
? '<span class="pill pill-info">系统</span>'
|
||||
: '<span class="tag">人工</span>';
|
||||
tr.innerHTML =
|
||||
`<td class="cell-main">${item.company_name}</td>` +
|
||||
`<td>${sourceCell}</td>` +
|
||||
`<td><span class="pill ${reminderTypePill(item.rule_key)}">${item.display_type}</span></td>` +
|
||||
`<td class="wrap">${summary.replace(/</g, "<")}</td>` +
|
||||
`<td class="num-col">${formatReminderTime(item.last_sent_at)}</td>` +
|
||||
`<td class="num-col">${item.deadline || "—"}</td>` +
|
||||
`<td><span class="pill ${reminderStatusPill(item.status_ui)}">${reminderStatusLabel(item.status_ui)}</span></td>` +
|
||||
'<td><div class="row" style="gap:6px;"><button type="button" class="btn btn-sm act-remind">再提醒</button><button type="button" class="btn btn-sm btn-ghost act-detail">详情</button></div></td>';
|
||||
return tr;
|
||||
}));
|
||||
refreshReminderCounts(result.stats || {});
|
||||
}
|
||||
|
||||
function refreshReminderCounts(stats) {
|
||||
const rows = $$("#reminder-tbody tr");
|
||||
let all = rows.length, sys = 0, man = 0;
|
||||
rows.forEach((r) => { if (r.dataset.source === "system") sys++; else man++; });
|
||||
$("#count-all").textContent = all;
|
||||
$("#count-system").textContent = sys;
|
||||
$("#count-manual").textContent = man;
|
||||
$("#table-foot-count").textContent = `共 ${all} 条提醒记录`;
|
||||
if (stats) {
|
||||
$("#table-foot-state").textContent = `未读 ${stats.unread || 0} · 处理中 ${stats.doing || 0} · 已完成 ${stats.done || 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPendingReminders(keys) {
|
||||
const response = await fetch("/api/admin/reminders/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dedupe_keys: keys }),
|
||||
}).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return null; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response?.ok) {
|
||||
showToast("发送失败", result?.message || "请稍后重试", "danger");
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function openReminderDetailDrawer(reminderId) {
|
||||
fetch(`/api/admin/reminders/${reminderId}`)
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
const item = result.reminder;
|
||||
if (!item) return;
|
||||
$("#rd-title").textContent = item.title;
|
||||
$("#rd-sub").textContent = item.content;
|
||||
const sourcePill = $("#rd-source-pill");
|
||||
if (sourcePill) {
|
||||
sourcePill.textContent = item.source === "auto" ? "系统" : "人工";
|
||||
sourcePill.className = item.source === "auto" ? "pill pill-info" : "tag";
|
||||
}
|
||||
$("#rd-fields").innerHTML =
|
||||
`<dt>公司</dt><dd>${item.company_name}</dd>` +
|
||||
`<dt>类型</dt><dd>${item.display_type}</dd>` +
|
||||
`<dt>触发原因</dt><dd>${item.rule_params?.pending_count ? `待处理 ${item.rule_params.pending_count} 项` : (item.rule_params?.period || "—")}</dd>` +
|
||||
`<dt>发送时间</dt><dd>${formatReminderTime(item.last_sent_at)}</dd>` +
|
||||
`<dt>催办次数</dt><dd>${item.send_count}</dd>` +
|
||||
`<dt>处理状态</dt><dd>${reminderStatusLabel(item.status_ui)}</dd>`;
|
||||
$("#rd-events").innerHTML = (item.events || []).map((ev) =>
|
||||
`<div class="list-row"><div class="lr-main"><div class="lr-title">${ev.event_type}</div><div class="lr-sub meta">${formatReminderTime(ev.created_at)} · ${ev.actor}${ev.detail ? " · " + ev.detail : ""}</div></div></div>`,
|
||||
).join("") || '<div class="empty"><div class="e-title">暂无事件</div></div>';
|
||||
$("#reminder-detail-drawer")?.classList.add("is-open");
|
||||
})
|
||||
.catch(() => showToast("加载详情失败", "", "danger"));
|
||||
}
|
||||
|
||||
function initAdminReminders() {
|
||||
loadAdminRemindersPending();
|
||||
loadAdminRemindersHistory("all");
|
||||
|
||||
$("#reminder-scan-btn")?.addEventListener("click", async () => {
|
||||
const response = await fetch("/api/admin/reminders/scan", { method: "POST" }).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response?.ok) {
|
||||
showToast("扫描失败", result?.message || "请稍后重试", "danger");
|
||||
return;
|
||||
}
|
||||
await loadAdminRemindersPending();
|
||||
showToast("扫描完成", `发现 ${result.summary?.items || 0} 项待提醒`, "success");
|
||||
});
|
||||
|
||||
$("#pending-send-all")?.addEventListener("click", async () => {
|
||||
const checked = $$(".pending-check:checked");
|
||||
let keys;
|
||||
if (checked.length) {
|
||||
keys = checked.map((input) => input.closest(".list-row")?.dataset.dedupeKey).filter(Boolean);
|
||||
} else {
|
||||
keys = JSON.parse($("#pending-send-all").dataset.keys || "[]");
|
||||
}
|
||||
if (!keys.length) return;
|
||||
const result = await sendPendingReminders(keys);
|
||||
if (!result) return;
|
||||
showToast(checked.length ? "已发送选中提醒" : "已全部发送", `共 ${result.count} 项`, "success");
|
||||
await loadAdminRemindersPending();
|
||||
await loadAdminRemindersHistory($("#reminder-tabs .active")?.dataset.filter || "all");
|
||||
});
|
||||
|
||||
$("#pending-list")?.addEventListener("click", async (event) => {
|
||||
const btn = event.target.closest(".pending-send-one");
|
||||
if (!btn) return;
|
||||
const row = btn.closest(".list-row");
|
||||
const key = row?.dataset.dedupeKey;
|
||||
if (!key) return;
|
||||
const result = await sendPendingReminders([key]);
|
||||
if (!result) return;
|
||||
if (!motionQuery.matches) {
|
||||
row.style.transition = "opacity 0.2s ease";
|
||||
row.style.opacity = "0";
|
||||
setTimeout(() => row.remove(), 200);
|
||||
} else {
|
||||
row.remove();
|
||||
}
|
||||
showToast("已发送提醒", "", "success");
|
||||
await loadAdminRemindersPending();
|
||||
await loadAdminRemindersHistory($("#reminder-tabs .active")?.dataset.filter || "all");
|
||||
});
|
||||
|
||||
$("#pending-list")?.addEventListener("change", (event) => {
|
||||
if (!event.target.classList.contains("pending-check")) return;
|
||||
const checked = $$(".pending-check:checked").length;
|
||||
const sendAll = $("#pending-send-all");
|
||||
if (!sendAll) return;
|
||||
sendAll.textContent = checked ? `发送选中(${checked})` : "全部一键发送";
|
||||
});
|
||||
|
||||
$("#reminderForm")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const checked = Array.prototype.slice.call(form.querySelectorAll('input[name="company"]:checked'));
|
||||
const type = $("#reminder-type").value;
|
||||
const content = $("#reminder-content").value.trim();
|
||||
const deadline = $("#reminder-deadline").value;
|
||||
$("#company-error").style.display = checked.length ? "none" : "";
|
||||
$("#content-error").style.display = content ? "none" : "";
|
||||
if (!checked.length || !content) return;
|
||||
for (const input of checked) {
|
||||
const response = await fetch("/api/admin/reminders/manual", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
company_id: Number(input.value),
|
||||
display_type: type,
|
||||
content,
|
||||
deadline: deadline || null,
|
||||
}),
|
||||
}).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response?.ok) {
|
||||
showToast("发送失败", result?.message || "请稍后重试", "danger");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const companies = checked.map((c) => {
|
||||
const company = (state.companies || []).find((item) => String(item.id) === c.value);
|
||||
return company?.name || c.value;
|
||||
}).join("、");
|
||||
form.querySelectorAll('input[name="company"]').forEach((c) => { c.checked = false; });
|
||||
const sendHint = $("#send-hint");
|
||||
sendHint.style.display = "";
|
||||
sendHint.textContent = `已发送给 ${companies},共 ${checked.length} 家公司。`;
|
||||
clearTimeout(sendHint._t);
|
||||
sendHint._t = setTimeout(() => { sendHint.style.display = "none"; }, 4000);
|
||||
showToast("人工提醒已发送", companies, "success");
|
||||
await loadAdminRemindersHistory($("#reminder-tabs .active")?.dataset.filter || "all");
|
||||
});
|
||||
|
||||
$("#reminder-tabs")?.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest("button[data-filter]");
|
||||
if (!btn) return;
|
||||
$("#reminder-tabs").querySelectorAll("button").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
loadAdminRemindersHistory(btn.dataset.filter);
|
||||
});
|
||||
|
||||
$("#reminder-tbody")?.addEventListener("click", async (event) => {
|
||||
const detailBtn = event.target.closest(".act-detail");
|
||||
if (detailBtn) {
|
||||
const tr = detailBtn.closest("tr");
|
||||
openReminderDetailDrawer(tr.dataset.reminderId);
|
||||
return;
|
||||
}
|
||||
const remindBtn = event.target.closest(".act-remind");
|
||||
if (!remindBtn) return;
|
||||
const tr = remindBtn.closest("tr");
|
||||
const response = await fetch(`/api/admin/reminders/${tr.dataset.reminderId}/resend`, { method: "POST" }).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response?.ok) {
|
||||
showToast("再提醒失败", result?.message || "请稍后重试", "danger");
|
||||
return;
|
||||
}
|
||||
remindBtn.textContent = "已再提醒";
|
||||
remindBtn.disabled = true;
|
||||
await loadAdminRemindersHistory($("#reminder-tabs .active")?.dataset.filter || "all");
|
||||
showToast("已再次发送提醒", "", "success");
|
||||
});
|
||||
|
||||
$$("[data-close-rd]").forEach((btn) => btn.addEventListener("click", () => {
|
||||
$("#reminder-detail-drawer")?.classList.remove("is-open");
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadAdminCompanies() {
|
||||
const tbody = $("#companyTable tbody");
|
||||
showTableLoading(tbody, 6);
|
||||
@@ -1787,7 +2088,7 @@ async function loadAdminCompanies() {
|
||||
const companies = result?.companies || [];
|
||||
state.companies = companies;
|
||||
renderAdminCompanyTable(companies);
|
||||
fillCompanySelects(companies.map((company) => company.name));
|
||||
fillCompanySelects(companies.map((company) => company.name), companies);
|
||||
if (companies.length >= 2) setPair(companies[0].name, companies[1].name);
|
||||
}
|
||||
|
||||
@@ -2194,7 +2495,18 @@ function initAdmin() {
|
||||
if (tip) { tip.style.display = ""; tip.style.color = "var(--success)"; tip.textContent = "已保存 · 立即生效"; }
|
||||
applySystemSettings(result.settings || {});
|
||||
await loadCalculationSettings();
|
||||
showToast("系统计算口径已保存", "结账日与起算日变更已留痕", "success");
|
||||
const gapDays = parseInt($("#cs-remind-days")?.value, 10) || 5;
|
||||
const reminderResp = await fetch("/api/admin/reminder-settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ settings: { monthly_start_day: String(day || 5), gap_days: String(gapDays) } }),
|
||||
}).catch(() => null);
|
||||
if (reminderResp?.status === 401) { window.location.href = "index.html"; return; }
|
||||
if (!reminderResp?.ok) {
|
||||
showToast("提醒扫描参数保存失败", "", "danger");
|
||||
return;
|
||||
}
|
||||
showToast("系统计算口径已保存", "结账日与起算日变更已留痕 · 提醒扫描参数已同步更新", "success");
|
||||
});
|
||||
|
||||
$("#runClosingCheck")?.addEventListener("click", () => {
|
||||
@@ -2319,173 +2631,9 @@ function initAdmin() {
|
||||
showToast(confirmBtn ? "期初已确认" : "期初已作废", "变更已留痕", "success");
|
||||
});
|
||||
|
||||
// ── 提醒管理:选公司 → 自动列出待提醒事项 → 一键发送 ──
|
||||
const reminderSelect = $("#reminder-company");
|
||||
const reminderItems = $("#reminder-items");
|
||||
const reminderError = $("#reminder-error");
|
||||
const companyError = $("#company-error");
|
||||
const sendBtn = $("#send-btn");
|
||||
const esc = (text) => String(text ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const fmtReminderTime = (iso) => String(iso || "").replace("T", " ").slice(5, 16);
|
||||
let pendingReminderItems = [];
|
||||
|
||||
function renderPendingItems(items) {
|
||||
pendingReminderItems = items || [];
|
||||
if (!reminderItems) return;
|
||||
if (!pendingReminderItems.length) {
|
||||
reminderItems.innerHTML = '<div class="empty">该公司当前没有待提醒事项。</div>';
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
reminderItems.replaceChildren(...pendingReminderItems.map((item) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "list-row";
|
||||
row.innerHTML =
|
||||
'<span class="pill pill-warn" style="flex:none;">待处理</span>' +
|
||||
'<div class="lr-main"><div class="lr-title">' + esc(item.kind) + '</div>' +
|
||||
'<div class="lr-sub">' + esc(item.content) + '</div></div>';
|
||||
return row;
|
||||
}));
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
}
|
||||
|
||||
async function loadPendingReminders(companyId) {
|
||||
if (!companyId) { renderPendingItems([]); return; }
|
||||
if (reminderItems) reminderItems.innerHTML = '<div class="loading-row">正在列出待提醒事项…</div>';
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
const response = await fetch("/api/admin/reminders/pending?company_id=" + encodeURIComponent(companyId)).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response || !response.ok) { renderPendingItems([]); return; }
|
||||
renderPendingItems(result.items || []);
|
||||
}
|
||||
|
||||
reminderSelect?.addEventListener("change", () => {
|
||||
if (reminderError) reminderError.style.display = "none";
|
||||
loadPendingReminders(reminderSelect.value);
|
||||
});
|
||||
|
||||
$("#reminderForm")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const companyId = reminderSelect?.value;
|
||||
if (companyError) companyError.style.display = companyId ? "none" : "";
|
||||
if (!companyId) return;
|
||||
if (!pendingReminderItems.length) {
|
||||
if (reminderError) reminderError.style.display = "";
|
||||
return;
|
||||
}
|
||||
if (reminderError) reminderError.style.display = "none";
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
const response = await fetch("/api/admin/reminders/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ company_id: Number(companyId) }),
|
||||
}).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response || !response.ok) {
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
showToast("发送失败", result?.message || "请稍后重试", "danger");
|
||||
return;
|
||||
}
|
||||
const sendHint = $("#send-hint");
|
||||
if (sendHint) {
|
||||
sendHint.style.display = "";
|
||||
sendHint.textContent = `已向 ${result.company_name} 发送 ${(result.reminders || []).length} 项提醒,截止 ${result.deadline || "—"}。`;
|
||||
clearTimeout(sendHint._t);
|
||||
sendHint._t = setTimeout(() => { sendHint.style.display = "none"; }, 4000);
|
||||
}
|
||||
await loadReminderHistory();
|
||||
await loadPendingReminders(companyId);
|
||||
showToast("提醒已发送", `已生成 ${(result.reminders || []).length} 条提醒记录`, "success");
|
||||
});
|
||||
|
||||
// ── 提醒历史(实时从后端读取) ──
|
||||
let reminderFilter = "all";
|
||||
function refreshReminderCounts() {
|
||||
const rows = $$("#reminder-tbody tr");
|
||||
let all = rows.length, sys = 0, man = 0;
|
||||
rows.forEach((r) => { if (r.dataset.source === "system") sys++; else man++; });
|
||||
const countAll = $("#count-all"), countSys = $("#count-system"), countMan = $("#count-manual");
|
||||
if (countAll) countAll.textContent = all;
|
||||
if (countSys) countSys.textContent = sys;
|
||||
if (countMan) countMan.textContent = man;
|
||||
const foot = $("#table-foot-count");
|
||||
if (foot) foot.textContent = "共 " + all + " 条提醒记录";
|
||||
const state = $("#table-foot-state");
|
||||
if (state) state.textContent = "未读 " + all + " · 处理中 0 · 已完成 0";
|
||||
}
|
||||
|
||||
function applyReminderFilter() {
|
||||
$$("#reminder-tbody tr").forEach((r) => {
|
||||
r.style.display = (reminderFilter === "all" || r.dataset.source === reminderFilter) ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
async function loadReminderHistory() {
|
||||
const tbody = $("#reminder-tbody");
|
||||
if (!tbody) return;
|
||||
const response = await fetch("/api/admin/reminders").catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
const reminders = result?.reminders || [];
|
||||
tbody.replaceChildren(...reminders.map((reminder) => {
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.source = reminder.source;
|
||||
row.innerHTML =
|
||||
'<td class="cell-main">' + esc(reminder.company_name) + '</td>' +
|
||||
'<td><span class="tag">' + esc(reminder.kind) + '</span></td>' +
|
||||
'<td class="wrap">' + esc(reminder.content) + '</td>' +
|
||||
'<td class="meta">' + fmtReminderTime(reminder.created_at) + '<span class="cell-sub">' + (reminder.source === "manual" ? "人工" : "系统") + (reminder.actor_username ? " · " + esc(reminder.actor_username) : "") + '</span></td>' +
|
||||
'<td class="meta">' + (reminder.deadline || "—") + '</td>' +
|
||||
'<td><span class="pill pill-danger">未读</span></td>' +
|
||||
'<td><button type="button" class="btn btn-sm btn-ghost act-history" data-company-id="' + reminder.company_id + '" data-company="' + esc(reminder.company_name) + '">查看历史</button></td>';
|
||||
return row;
|
||||
}));
|
||||
applyReminderFilter();
|
||||
refreshReminderCounts();
|
||||
}
|
||||
|
||||
$("#reminder-tabs")?.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest("button[data-filter]");
|
||||
if (!btn) return;
|
||||
$("#reminder-tabs").querySelectorAll("button").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
reminderFilter = btn.dataset.filter;
|
||||
applyReminderFilter();
|
||||
});
|
||||
|
||||
$("#reminder-tbody")?.addEventListener("click", (event) => {
|
||||
const historyBtn = event.target.closest(".act-history");
|
||||
if (historyBtn) openReminderHistory(historyBtn.dataset.companyId, historyBtn.dataset.company);
|
||||
});
|
||||
|
||||
async function openReminderHistory(companyId, companyName) {
|
||||
$("#history-title").textContent = (companyName || "") + " · 提醒历史";
|
||||
$("#history-sub").textContent = "该公司的全部提醒触达记录,按时间倒序。";
|
||||
const list = $("#history-list");
|
||||
list.innerHTML = '<div class="loading-row">加载中…</div>';
|
||||
const response = await fetch("/api/admin/reminders?company_id=" + encodeURIComponent(companyId || "")).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
const items = result?.reminders || [];
|
||||
if (!items.length) {
|
||||
list.innerHTML = '<div class="empty"><div class="e-title">暂无历史记录</div>该公司尚未收到过提醒。</div>';
|
||||
} else {
|
||||
list.innerHTML = items.map((it) => {
|
||||
return '<div class="list-row">' +
|
||||
'<span class="tag">' + esc(it.kind) + '</span>' +
|
||||
'<div class="lr-main"><div class="lr-title">' + esc(it.content) + '</div><div class="lr-sub"><span class="meta">' + fmtReminderTime(it.created_at) + ' · ' + (it.source === "manual" ? "人工" : "系统") + (it.actor_username ? " · " + esc(it.actor_username) : "") + '</span></div></div>' +
|
||||
'<div class="lr-side"><span class="pill pill-danger">未读</span></div></div>';
|
||||
}).join("");
|
||||
}
|
||||
openModal("history-modal");
|
||||
}
|
||||
$("#history-close")?.addEventListener("click", () => closeModal("history-modal"));
|
||||
$("#history-ok")?.addEventListener("click", () => closeModal("history-modal"));
|
||||
initAdminReminders();
|
||||
|
||||
loadSystemSettings();
|
||||
loadReminderHistory();
|
||||
}
|
||||
|
||||
const FLOW_DEMO = [
|
||||
@@ -3287,34 +3435,60 @@ function initReconcile() {
|
||||
tabSubject?.addEventListener("click", () => switchTab("subject"));
|
||||
}
|
||||
|
||||
function renderCompanyNoticeRow(item) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "list-row";
|
||||
row.dataset.status = item.status_ui;
|
||||
row.dataset.reminderId = String(item.id);
|
||||
const titleWeight = item.status_ui === "unread" ? "650" : "400";
|
||||
const titleColor = item.status_ui === "unread" ? "var(--fg)" : "var(--muted)";
|
||||
const sourceTag = item.source === "auto" ? '<span class="pill pill-info">系统</span>' : '<span class="tag">管理员</span>';
|
||||
const actionLink = item.action_link;
|
||||
let side = "";
|
||||
if (item.status_ui === "unread") {
|
||||
side = '<button class="btn btn-sm btn-mark-read">标记已读</button>';
|
||||
} else if (item.status_ui === "doing" && actionLink) {
|
||||
side = `<button class="btn btn-sm btn-primary" data-view-link="${actionLink}">去处理</button>`;
|
||||
} else if (item.status_ui === "doing") {
|
||||
side = '<button class="btn btn-sm btn-ghost btn-mark-done">标记完成</button>';
|
||||
}
|
||||
row.innerHTML =
|
||||
`<span class="pill ${reminderStatusPill(item.status_ui)}" style="flex:none;">${reminderStatusLabel(item.status_ui)}</span>` +
|
||||
sourceTag +
|
||||
`<div class="lr-main"><div class="lr-title" style="font-weight:${titleWeight};color:${titleColor};">${item.title}</div>` +
|
||||
`<div class="lr-sub"><span class="meta">${formatReminderTime(item.last_sent_at)}</span> · ${item.content}</div></div>` +
|
||||
`<div class="lr-side">${side}</div>`;
|
||||
return row;
|
||||
}
|
||||
|
||||
function initNotifications() {
|
||||
const tabs = $("#notice-tabs");
|
||||
const list = $("#notice-list");
|
||||
if (!tabs || !list) return;
|
||||
const rows = $$(".list-row", list);
|
||||
const emptyBox = $("#notice-empty");
|
||||
let currentFilter = "all";
|
||||
let rows = [];
|
||||
|
||||
const STATUS_PILL = { unread: "pill-danger", doing: "pill-warn", done: "pill-success" };
|
||||
const STATUS_LABEL = { unread: "未读", doing: "处理中", done: "已完成" };
|
||||
|
||||
function counts() {
|
||||
const c = { all: rows.length, unread: 0, doing: 0, done: 0 };
|
||||
rows.forEach((row) => { c[row.getAttribute("data-status")] += 1; });
|
||||
return c;
|
||||
}
|
||||
|
||||
function refreshCounts() {
|
||||
const c = counts();
|
||||
function refreshCounts(stats, unreadCount) {
|
||||
const c = stats || { all: rows.length, unread: 0, doing: 0, done: 0 };
|
||||
if (!stats) {
|
||||
c.all = rows.length;
|
||||
rows.forEach((row) => { c[row.getAttribute("data-status")] += 1; });
|
||||
} else {
|
||||
c.all = rows.length;
|
||||
}
|
||||
$("#count-all").textContent = c.all;
|
||||
$("#count-unread").textContent = c.unread;
|
||||
$("#count-doing").textContent = c.doing;
|
||||
$("#count-done").textContent = c.done;
|
||||
const navBadge = $('.side-nav a[data-view="notifications"] .nav-badge');
|
||||
const unread = unreadCount ?? c.unread;
|
||||
if (navBadge) {
|
||||
navBadge.textContent = c.unread;
|
||||
navBadge.style.display = c.unread ? "" : "none";
|
||||
navBadge.textContent = unread;
|
||||
navBadge.style.display = unread ? "" : "none";
|
||||
}
|
||||
const wsLink = $("#workspace-notice-link");
|
||||
if (wsLink) wsLink.textContent = unread ? `全部通知 (${c.all}) →` : "全部通知 →";
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
@@ -3324,15 +3498,35 @@ function initNotifications() {
|
||||
row.style.display = show ? "" : "none";
|
||||
if (show) visible += 1;
|
||||
});
|
||||
emptyBox.style.display = visible === 0 ? "" : "none";
|
||||
if (emptyBox) emptyBox.style.display = visible === 0 ? "" : "none";
|
||||
}
|
||||
|
||||
function setStatus(row, status) {
|
||||
row.setAttribute("data-status", status);
|
||||
const pill = $(".pill", row);
|
||||
if (pill) {
|
||||
pill.className = `pill ${STATUS_PILL[status]}`;
|
||||
pill.textContent = STATUS_LABEL[status];
|
||||
async function loadReminders() {
|
||||
const response = await fetch("/api/company/reminders").catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
const result = await response?.json().catch(() => ({}));
|
||||
if (!response?.ok) return;
|
||||
const items = result.reminders || [];
|
||||
list.replaceChildren(...items.map((item) => renderCompanyNoticeRow(item)));
|
||||
rows = $$(".list-row", list);
|
||||
refreshCounts(result.stats, result.unread_count);
|
||||
applyFilter();
|
||||
const wsList = $("#workspace-notice-list");
|
||||
if (wsList) {
|
||||
const preview = items.slice(0, 3);
|
||||
if (!preview.length) {
|
||||
wsList.innerHTML = '<div class="empty"><div class="e-title">暂无通知</div></div>';
|
||||
} else {
|
||||
wsList.replaceChildren(...preview.map((item) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "list-row";
|
||||
row.innerHTML =
|
||||
`<span class="pill ${reminderTypePill(item.rule_key)}">${item.display_type}</span>` +
|
||||
`<div class="lr-main"><div class="lr-title">${item.title}</div><div class="lr-sub">${item.content.slice(0, 48)}</div></div>` +
|
||||
`<span class="lr-side meta">${formatReminderTime(item.last_sent_at).slice(5, 10)}</span>`;
|
||||
return row;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3346,29 +3540,38 @@ function initNotifications() {
|
||||
applyFilter();
|
||||
});
|
||||
|
||||
list.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest(".btn-mark-read");
|
||||
list.addEventListener("click", async (event) => {
|
||||
const viewLink = event.target.closest("[data-view-link]");
|
||||
if (viewLink && list.contains(viewLink)) {
|
||||
event.preventDefault();
|
||||
showView(viewLink.dataset.viewLink);
|
||||
return;
|
||||
}
|
||||
const readBtn = event.target.closest(".btn-mark-read");
|
||||
const doneBtn = event.target.closest(".btn-mark-done");
|
||||
const btn = readBtn || doneBtn;
|
||||
if (!btn) return;
|
||||
const row = btn.closest(".list-row");
|
||||
setStatus(row, "done");
|
||||
btn.remove();
|
||||
refreshCounts();
|
||||
applyFilter();
|
||||
const id = row?.dataset.reminderId;
|
||||
if (!id) return;
|
||||
const action = readBtn ? "acknowledge" : "resolve";
|
||||
const response = await fetch(`/api/company/reminders/${id}/${action}`, { method: "POST" }).catch(() => null);
|
||||
if (response?.status === 401) { window.location.href = "index.html"; return; }
|
||||
if (!response?.ok) return;
|
||||
await loadReminders();
|
||||
});
|
||||
|
||||
$("#mark-all-read")?.addEventListener("click", () => {
|
||||
rows.forEach((row) => {
|
||||
if (row.getAttribute("data-status") !== "unread") return;
|
||||
setStatus(row, "done");
|
||||
$(".btn-mark-read", row)?.remove();
|
||||
});
|
||||
refreshCounts();
|
||||
applyFilter();
|
||||
$("#mark-all-read")?.addEventListener("click", async () => {
|
||||
const unreadRows = rows.filter((row) => row.getAttribute("data-status") === "unread");
|
||||
for (const row of unreadRows) {
|
||||
const id = row.dataset.reminderId;
|
||||
await fetch(`/api/company/reminders/${id}/acknowledge`, { method: "POST" }).catch(() => null);
|
||||
}
|
||||
await loadReminders();
|
||||
showToast("通知已全部标为已读", "", "success");
|
||||
});
|
||||
|
||||
refreshCounts();
|
||||
applyFilter();
|
||||
loadReminders();
|
||||
}
|
||||
|
||||
function openAccountDetail(account) {
|
||||
|
||||
+10
-109
@@ -26,7 +26,7 @@
|
||||
<a data-view="reconcile" href="#reconcile"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M9 11.5l2 2 4-4.5"/><rect x="4" y="3" width="16" height="18" rx="2"/></svg><span class="nav-label">往来确认</span><span class="nav-badge">5</span></a>
|
||||
<div class="nav-group">账户与消息</div>
|
||||
<a data-view="accounts" href="#accounts"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/></svg><span class="nav-label">银行账户</span></a>
|
||||
<a data-view="notifications" href="#notifications"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M6 9a6 6 0 1 1 12 0c0 5 2 6 2 6H4s2-1 2-6"/><path d="M10 19a2 2 0 0 0 4 0"/></svg><span class="nav-label">通知</span><span class="nav-badge">3</span></a>
|
||||
<a data-view="notifications" href="#notifications"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M6 9a6 6 0 1 1 12 0c0 5 2 6 2 6H4s2-1 2-6"/><path d="M10 19a2 2 0 0 0 4 0"/></svg><span class="nav-label">通知</span><span class="nav-badge" id="notice-nav-badge" style="display: none;">0</span></a>
|
||||
</nav>
|
||||
<div class="side-foot">
|
||||
<div class="user-row">
|
||||
@@ -181,31 +181,10 @@
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<span class="card-title">最新通知<span class="sub">来自管理端</span></span>
|
||||
<button class="btn btn-sm btn-ghost" data-view-link="notifications">全部通知 (3) →</button>
|
||||
<button class="btn btn-sm btn-ghost" data-view-link="notifications" id="workspace-notice-link">全部通知 →</button>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="pill pill-warn">提醒</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">7 月账期结账日顺延至 08-29</div>
|
||||
<div class="lr-sub">请在此之前完成往来确认事项</div>
|
||||
</div>
|
||||
<span class="lr-side meta">08-18</span>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="pill pill-warn">提醒</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">3 笔单边流水待确认</div>
|
||||
<div class="lr-sub">涉及金牛置业、金牛贸易往来</div>
|
||||
</div>
|
||||
<span class="lr-side meta">08-16</span>
|
||||
</div>
|
||||
<div class="list-row">
|
||||
<span class="pill pill-success">通过</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">中行尾号 9916 流水导入成功</div>
|
||||
<div class="lr-sub">流水覆盖至 08-12</div>
|
||||
</div>
|
||||
<span class="lr-side meta">08-12</span>
|
||||
<div id="workspace-notice-list">
|
||||
<div class="empty"><div class="e-title">暂无通知</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -804,93 +783,15 @@
|
||||
|
||||
<div class="card">
|
||||
<div class="tabs" id="notice-tabs">
|
||||
<button type="button" class="active" data-filter="all" aria-pressed="true">全部<span class="tab-count" id="count-all">9</span></button>
|
||||
<button type="button" data-filter="unread" aria-pressed="false">未读<span class="tab-count" id="count-unread">3</span></button>
|
||||
<button type="button" data-filter="doing" aria-pressed="false">处理中<span class="tab-count" id="count-doing">2</span></button>
|
||||
<button type="button" data-filter="done" aria-pressed="false">已完成<span class="tab-count" id="count-done">4</span></button>
|
||||
<button type="button" class="active" data-filter="all" aria-pressed="true">全部<span class="tab-count" id="count-all">0</span></button>
|
||||
<button type="button" data-filter="unread" aria-pressed="false">未读<span class="tab-count" id="count-unread">0</span></button>
|
||||
<button type="button" data-filter="doing" aria-pressed="false">处理中<span class="tab-count" id="count-doing">0</span></button>
|
||||
<button type="button" data-filter="done" aria-pressed="false">已完成<span class="tab-count" id="count-done">0</span></button>
|
||||
</div>
|
||||
|
||||
<div id="notice-list">
|
||||
<div class="list-row" data-status="unread">
|
||||
<span class="pill pill-danger" style="flex: none;">未读</span>
|
||||
<span class="tag">系统</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">7 月流水存在断档风险:交行 尾号 7710 未上传</div>
|
||||
<div class="lr-sub"><span class="meta">2026-08-20 14:32</span> · 交行 7710 账户(待审核)7 月流水尚未上传,距 7 月结账日(顺延至 08-29)仅剩 9 天,请尽快补传。</div>
|
||||
</div>
|
||||
<div class="lr-side"><button class="btn btn-sm btn-mark-read">标记已读</button></div>
|
||||
</div>
|
||||
<div class="list-row" data-status="unread">
|
||||
<span class="pill pill-danger" style="flex: none;">未读</span>
|
||||
<span class="tag">系统</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">您有 2 笔单边流水待选择对方证据</div>
|
||||
<div class="lr-sub"><span class="meta">2026-08-19 09:15</span> · 与金牛置业的煤炭采购款 ¥3,200,000.00 等 2 笔流水仅有本方记录,需选择对方银行流水佐证。</div>
|
||||
</div>
|
||||
<div class="lr-side"><button class="btn btn-sm btn-mark-read">标记已读</button></div>
|
||||
</div>
|
||||
<div class="list-row" data-status="unread">
|
||||
<span class="pill pill-danger" style="flex: none;">未读</span>
|
||||
<span class="tag">管理员</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">管理员提醒:请于 08-29 前完成待确认事项</div>
|
||||
<div class="lr-sub"><span class="meta">2026-08-18 16:40</span> · 7 月账期结账顺延至 08-29,贵公司当前仍有 5 项往来确认未完成,请合理安排时间。</div>
|
||||
</div>
|
||||
<div class="lr-side"><button class="btn btn-sm btn-mark-read">标记已读</button></div>
|
||||
</div>
|
||||
<div class="list-row" data-status="doing">
|
||||
<span class="pill pill-warn" style="flex: none;">处理中</span>
|
||||
<span class="tag">系统</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">手工记录科目待确认:其他应收 ¥86,500.00</div>
|
||||
<div class="lr-sub"><span class="meta">2026-08-15 10:02</span> · 07-28 录入的矿区备用金垫付记录,往来科目「其他应收」待管理员复核。</div>
|
||||
</div>
|
||||
<div class="lr-side"><button class="btn btn-sm btn-primary" data-view-link="manual">去处理</button></div>
|
||||
</div>
|
||||
<div class="list-row" data-status="doing">
|
||||
<span class="pill pill-warn" style="flex: none;">处理中</span>
|
||||
<span class="tag">管理员</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">往来确认回复:置业已确认 320 万煤炭采购款</div>
|
||||
<div class="lr-sub"><span class="meta">2026-08-12 11:26</span> · 金牛置业已确认 07-14 煤炭采购款 ¥3,200,000.00,请补充本方工行 3305 账户流水证据完成闭环。</div>
|
||||
</div>
|
||||
<div class="lr-side"><button class="btn btn-sm btn-primary" data-view-link="reconcile">去处理</button></div>
|
||||
</div>
|
||||
<div class="list-row" data-status="done">
|
||||
<span class="pill pill-success" style="flex: none;">已完成</span>
|
||||
<span class="tag">系统</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">流水导入成功:中行 尾号 9916 · 42 笔</div>
|
||||
<div class="lr-sub"><span class="meta">2026-08-06 15:52</span> · 7 月中行一般户流水已导入,42 笔全部校验通过,无重复记录。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-row" data-status="done">
|
||||
<span class="pill pill-success" style="flex: none;">已完成</span>
|
||||
<span class="tag">系统</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">流水导入成功:工行 尾号 3305 · 86 笔</div>
|
||||
<div class="lr-sub"><span class="meta">2026-08-06 15:48</span> · 7 月工行基本户流水已导入,86 笔全部校验通过,无重复记录。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-row" data-status="done">
|
||||
<span class="pill pill-success" style="flex: none;">已完成</span>
|
||||
<span class="tag">管理员</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">交行 尾号 7710 账户开户资料已受理</div>
|
||||
<div class="lr-sub"><span class="meta">2026-07-22 13:10</span> · 新账户开户资料已提交管理员,审核通过后方可启用并上传流水。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-row" data-status="done">
|
||||
<span class="pill pill-success" style="flex: none;">已完成</span>
|
||||
<span class="tag">系统</span>
|
||||
<div class="lr-main">
|
||||
<div class="lr-title">2026 年 6 月账期已结账</div>
|
||||
<div class="lr-sub"><span class="meta">2026-07-05 09:00</span> · 6 月账期已按每月 5 日结账规则完成结账,期末数据已锁定,不可再修改。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="notice-list"></div>
|
||||
|
||||
<div class="empty" id="notice-empty" style="display: none;">
|
||||
<div class="empty" id="notice-empty">
|
||||
<div class="e-title">暂无该状态的通知</div>
|
||||
<div>切换其他状态查看,或等待新的系统与管理员消息。</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user