4064 lines
201 KiB
PHP
Executable File
4064 lines
201 KiB
PHP
Executable File
<?php
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/../app/auth.php';
|
||
require_once __DIR__ . '/../app/helpers.php';
|
||
require_once __DIR__ . '/../app/services/quick_queries.php';
|
||
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||
require_once __DIR__ . '/../vendor/autoload.php';
|
||
}
|
||
|
||
$pdo = db();
|
||
$action = $_GET['action'] ?? 'home';
|
||
if (!defined('ARCHIVE_KIND_BANK_LOAN')) {
|
||
define('ARCHIVE_KIND_BANK_LOAN', 'bank_loan');
|
||
define('ARCHIVE_KIND_EXTERNAL_GUARANTEE', 'external_guarantee');
|
||
}
|
||
|
||
function normalizeArchiveKind(?string $v): string
|
||
{
|
||
$v = trim((string) $v);
|
||
return $v === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? ARCHIVE_KIND_EXTERNAL_GUARANTEE : ARCHIVE_KIND_BANK_LOAN;
|
||
}
|
||
|
||
function archiveKindLabel(string $k): string
|
||
{
|
||
return $k === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? '对外担保' : '银行贷款';
|
||
}
|
||
|
||
if ($action === 'logout') {
|
||
session_destroy();
|
||
header('Location: index.php?action=login');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'login') {
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$username = trim((string)($_POST['username'] ?? ''));
|
||
$password = (string)($_POST['password'] ?? '');
|
||
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :u');
|
||
$stmt->execute([':u' => $username]);
|
||
$user = $stmt->fetch();
|
||
if ($user && password_verify($password, $user['password_hash'])) {
|
||
$_SESSION['uid'] = (int)$user['id'];
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
flash('error', '用户名或密码错误');
|
||
header('Location: index.php?action=login');
|
||
exit;
|
||
}
|
||
$flash = getFlash();
|
||
?>
|
||
<!doctype html>
|
||
<html lang="zh-CN"><head><meta charset="UTF-8"><title>金牛集团贷款档案管理系统 - 登录</title>
|
||
<style>
|
||
body{font-family:Inter,Arial,sans-serif;background:linear-gradient(130deg,#edf3ff 0%,#f8fbff 42%,#f1f5f9 100%);margin:0;color:#1f2937}
|
||
.box{max-width:420px;margin:88px auto;background:rgba(255,255,255,.95);padding:28px;border-radius:16px;box-shadow:0 18px 45px rgba(31,59,100,.14);backdrop-filter:blur(2px)}
|
||
h2{margin:0 0 8px;font-size:24px;color:#1f3b64}
|
||
.sub{margin:0 0 16px;color:#64748b;font-size:14px}
|
||
input{width:100%;padding:11px 12px;margin:8px 0;border:1px solid #d2d9e6;border-radius:10px;box-sizing:border-box;background:#fff}
|
||
input:focus{border-color:#2f5da8;outline:none;box-shadow:0 0 0 3px rgba(47,93,168,.15)}
|
||
button{width:100%;background:#1f3b64;color:#fff;border:0;padding:11px;border-radius:10px;cursor:pointer;font-weight:600}
|
||
button:hover{background:#234578}
|
||
.err{background:#ffe8e8;color:#a40000;padding:8px;border-radius:6px}
|
||
</style>
|
||
</head><body>
|
||
<div class="box">
|
||
<h2>金牛集团贷款档案管理系统</h2>
|
||
<p class="sub">请输入账号密码登录系统</p>
|
||
<?php if ($flash): ?><div class="err"><?= e($flash['msg']) ?></div><?php endif; ?>
|
||
<form method="post">
|
||
<input name="username" placeholder="用户名" required>
|
||
<input type="password" name="password" placeholder="密码" required>
|
||
<button type="submit">登录</button>
|
||
</form>
|
||
</div>
|
||
</body></html>
|
||
<?php
|
||
exit;
|
||
}
|
||
|
||
requireLogin();
|
||
$me = currentUser();
|
||
if ($action === 'home') {
|
||
$qs = (string)($_SERVER['QUERY_STRING'] ?? '');
|
||
$_SESSION['last_list_url'] = 'index.php' . ($qs !== '' ? ('?' . $qs) : '');
|
||
}
|
||
|
||
function pageHeader(array $me): void
|
||
{
|
||
$flash = getFlash();
|
||
$lastListUrl = (string)($_SESSION['last_list_url'] ?? 'index.php');
|
||
if ($lastListUrl === '' || strpos($lastListUrl, 'index.php') !== 0) {
|
||
$lastListUrl = 'index.php';
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="zh-CN"><head><meta charset="UTF-8"><title>金牛集团贷款档案管理系统</title>
|
||
<style>
|
||
:root{
|
||
--bg:#f3f6fb;--text:#1f2937;--card:#fff;--line:#edf0f5;--head:#f8fafc;--muted:#6b7280;
|
||
--top1:#1d355c;--top2:#254776;--primary:#1f3b64;--primary-hover:#234578;
|
||
--input-bg:#fff;--input-border:#d8e0ea;--shadow:0 8px 24px rgba(15,23,42,.08);
|
||
}
|
||
body.dark{
|
||
--bg:#0f172a;--text:#e5e7eb;--card:#111827;--line:#1f2937;--head:#172033;--muted:#9ca3af;
|
||
--top1:#0b1222;--top2:#172441;--primary:#2a4c80;--primary-hover:#355d99;
|
||
--input-bg:#0b1324;--input-border:#2a3a55;--shadow:0 8px 24px rgba(0,0,0,.35);
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{max-width:100%;overflow-x:hidden}
|
||
body{font-family:Inter,Arial,sans-serif;background:var(--bg);margin:0;color:var(--text)}
|
||
.top{background:linear-gradient(90deg,var(--top1),var(--top2));color:#fff;padding:10px 10px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 3px 18px rgba(20,44,84,.22);position:sticky;top:0;z-index:40;gap:8px;flex-wrap:wrap}
|
||
.brand{font-size:18px;font-weight:700;letter-spacing:.2px}
|
||
.a{color:#fff;text-decoration:none;margin-left:12px;padding:7px 10px;border-radius:8px;transition:background .2s}
|
||
.a:hover{background:rgba(255,255,255,.16)}
|
||
.wrap{padding:12px 16px 6px;max-width:100%;margin:0 auto;width:100%;box-sizing:border-box}
|
||
.grid{display:grid;grid-template-columns:minmax(180px,250px) minmax(0,1fr);gap:12px;min-width:0}
|
||
.card{background:var(--card);border-radius:14px;padding:14px;box-shadow:var(--shadow);min-width:0;box-sizing:border-box}
|
||
table{width:100%;border-collapse:collapse}
|
||
th,td{border-bottom:1px solid var(--line);padding:7px 6px;font-size:13px;text-align:left}
|
||
th{color:#475569;font-weight:600;background:var(--head)}
|
||
input,select{padding:8px 10px;border:1px solid var(--input-border);border-radius:8px;background:var(--input-bg);color:var(--text)}
|
||
input:focus,select:focus{border-color:#2f5da8;outline:none;box-shadow:0 0 0 3px rgba(47,93,168,.12)}
|
||
button{padding:8px 12px;border:0;border-radius:8px;background:var(--primary);color:#fff;cursor:pointer}
|
||
button:hover{background:var(--primary-hover)}
|
||
.muted{color:var(--muted)}
|
||
.badge{padding:3px 9px;border-radius:999px;font-size:12px}
|
||
.b1{background:#e8f1ff;color:#1d4ed8}.b2{background:#eee;color:#374151}.b3{background:#fff;border:1px solid #e5e7eb;color:#374151}.b4{background:#fee2e2;color:#b91c1c}
|
||
.flash-ok{background:#e7ffe8;color:#0b7a1a;padding:8px;border-radius:6px;margin-bottom:8px}
|
||
.flash-err{background:#ffe8e8;color:#a40000;padding:8px;border-radius:6px;margin-bottom:8px}
|
||
.tree ul{list-style:none;padding-left:14px;margin:0}
|
||
.tree li{margin:2px 0;position:relative}
|
||
.inline{display:inline}
|
||
.small{font-size:12px}
|
||
.path{font-size:12px;color:var(--muted)}
|
||
.btn-danger{background:#b91c1c}
|
||
.btn-gray{background:#4b5563}
|
||
.table-wrap{overflow-x:hidden;max-width:100%;width:100%}
|
||
.table-wrap table{min-width:0;width:100%}
|
||
.table-wrap thead th{position:sticky;top:0;z-index:2}
|
||
.table-wrap th:last-child,.table-wrap td:last-child{position:sticky;right:0;background:var(--card);z-index:1}
|
||
.table-wrap thead th:last-child{z-index:3}
|
||
.fixed-table{table-layout:fixed}
|
||
.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.col-check{width:34px}
|
||
.col-kind{width:64px;text-align:center}
|
||
.badge-k-loan{font-size:11px;padding:2px 6px;border-radius:6px;background:#eef4ff;color:#1e40af;font-weight:600}
|
||
.badge-k-guar{font-size:11px;padding:2px 6px;border-radius:6px;background:#ecfdf5;color:#166534;font-weight:600}
|
||
.th-stack{display:flex;flex-direction:column;gap:2px;line-height:1.2;align-items:flex-start}
|
||
.th-sub{font-size:11px;font-weight:400;color:#64748b}
|
||
.col-name{width:108px}
|
||
.col-inst{width:96px}
|
||
.col-borrower{width:88px}
|
||
.col-guarantor{width:88px}
|
||
.col-amount{width:84px}
|
||
.col-date{width:68px}
|
||
.col-rate{width:46px}
|
||
.col-status{width:52px}
|
||
.col-action{width:138px}
|
||
.archive-form-grid{display:grid;grid-template-columns:1fr;gap:10px 12px;max-width:920px}
|
||
.archive-form-grid .form-actions{grid-column:1/-1;display:flex;gap:10px;align-items:center}
|
||
.archive-form-grid .full{grid-column:1/-1}
|
||
.archive-form-grid .form-row-2{display:grid;grid-template-columns:1fr 1fr;gap:10px 12px;align-items:start}
|
||
.archive-form-grid .form-row-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px 12px;align-items:start}
|
||
.archive-form-grid .form-row-overdue{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(52px,76px) minmax(0,1.15fr);gap:10px 12px;align-items:start}
|
||
.archive-form-grid .form-col-float .form-field-stack>label{font-size:12px;color:#64748b}
|
||
.archive-form-grid .form-col-float .form-field-stack input{text-align:center;font-size:13px;padding:6px 4px}
|
||
.archive-form-grid .form-field-stack{display:flex;flex-direction:column;gap:4px;min-width:0}
|
||
.archive-form-grid .form-field-stack>label{font-size:13px;color:#475569}
|
||
.archive-form-grid .form-field-stack input,.archive-form-grid .form-field-stack select,.archive-form-grid .form-field-stack textarea{width:100%;box-sizing:border-box}
|
||
.archive-form-grid .form-field-stack textarea{resize:vertical;min-height:72px;font-family:inherit;font-size:14px;line-height:1.45;padding:8px 10px;border-radius:8px;border:1px solid var(--input-border);background:var(--input-bg);color:var(--text)}
|
||
.archive-form-grid .date-labeled{display:flex;align-items:center;gap:10px;min-width:0}
|
||
.archive-form-grid .date-labeled .date-fixed-label{flex:0 0 auto;font-size:14px;color:var(--text);white-space:nowrap}
|
||
.archive-form-grid .date-labeled input[type=date]{flex:1 1 auto;min-width:0}
|
||
.toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px}
|
||
.toolbar-left{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||
.toolbar-right{display:flex;gap:10px;align-items:center}
|
||
.pill{background:#eef4ff;color:#1e3a8a;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:600}
|
||
.metric-wrap{display:grid;grid-template-columns:repeat(7,minmax(110px,1fr));gap:10px;margin-bottom:12px}
|
||
.metric{background:linear-gradient(180deg,#ffffff,#f8fbff);border:1px solid #e3eaf5;border-radius:12px;padding:10px}
|
||
.metric .k{font-size:12px;color:#64748b}
|
||
.metric .v{margin-top:4px;font-size:22px;color:#1f3b64;font-weight:700}
|
||
.metric.m-all{background:#f8fafc}
|
||
.metric.m-active{background:#eff6ff}
|
||
.metric.m-done{background:#f3f4f6}
|
||
.metric.m-unset{background:#ffffff}
|
||
.metric.m-due{background:#fff7ed}
|
||
.metric.m-plan{background:#ecfeff}
|
||
.metric.m-overdue{background:#fef2f2}
|
||
.tree{position:sticky;top:72px;max-height:calc(100vh - 90px);overflow:auto}
|
||
.section-title{margin:0 0 10px;font-size:18px;color:#1f3b64}
|
||
.subtle-link{color:#1d4ed8;text-decoration:none}
|
||
.subtle-link:hover{text-decoration:underline}
|
||
.toolbar-btn,.icon-btn{
|
||
display:inline-flex;align-items:center;justify-content:center;gap:4px;
|
||
height:28px;padding:0 10px;border:1px solid var(--input-border);
|
||
border-radius:8px;background:var(--input-bg);
|
||
color:var(--text);text-decoration:none;font-size:12px;
|
||
transition:all .2s ease;box-sizing:border-box;
|
||
}
|
||
.toolbar-btn:hover,.icon-btn:hover{border-color:#2f5da8;color:#1d4ed8;background:rgba(59,130,246,.06)}
|
||
.toolbar-btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}
|
||
.toolbar-btn.primary:hover{background:var(--primary-hover);border-color:var(--primary-hover);color:#fff}
|
||
.tree details{margin:2px 0}
|
||
.tree summary{cursor:pointer;color:var(--text);list-style:none}
|
||
.tree summary::-webkit-details-marker{display:none}
|
||
.tree li::before{content:"";position:absolute;left:-9px;top:-6px;bottom:-6px;border-left:1px solid var(--line)}
|
||
.tree li::after{content:"";position:absolute;left:-9px;top:15px;width:9px;border-top:1px solid var(--line)}
|
||
.tree > ul > li::before,.tree > ul > li::after{display:none}
|
||
.tree-item{display:flex;align-items:center;gap:6px;min-height:28px;padding:2px 4px;border-radius:8px}
|
||
.tree-item:hover{background:rgba(59,130,246,.06)}
|
||
.tree-toggle{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:4px;color:#64748b;font-size:11px;user-select:none}
|
||
details[open] > summary .tree-toggle{transform:rotate(90deg)}
|
||
.tree-link{color:var(--text);text-decoration:none;display:inline-block;max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.tree-link:hover{text-decoration:underline}
|
||
.tree-link.current{font-weight:700;color:#1d4ed8}
|
||
.tree-link.current::before{content:"";display:inline-block;width:3px;height:14px;background:#3b82f6;border-radius:3px;margin-right:6px;vertical-align:-2px}
|
||
.quick-links-col{display:flex;flex-direction:column;align-items:flex-start;gap:8px;min-width:0}
|
||
.quick-links-col .tree-link{padding:4px 2px}
|
||
.quick-menu-details{border:1px solid var(--line);border-radius:10px;background:var(--head);overflow:hidden}
|
||
.quick-menu-details>summary{list-style:none;cursor:pointer;padding:8px 10px;font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;justify-content:space-between;gap:8px;user-select:none}
|
||
.quick-menu-details>summary::-webkit-details-marker{display:none}
|
||
.quick-menu-details>summary::after{content:"▶";font-size:10px;color:var(--muted);transition:transform .2s}
|
||
.quick-menu-details[open]>summary::after{transform:rotate(90deg)}
|
||
.quick-menu-hint{font-weight:400;color:var(--muted);font-size:12px}
|
||
.quick-menu-details .quick-links-col{padding:4px 10px 12px}
|
||
.fin-tool-page{max-width:1100px}
|
||
.fin-two-col{display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,360px);gap:16px;align-items:start}
|
||
@media (max-width:900px){.fin-two-col{grid-template-columns:1fr}}
|
||
.fin-panel{border:1px solid var(--line);border-radius:12px;padding:14px;background:var(--card)}
|
||
.fin-panel h4{margin:0 0 10px;font-size:15px;color:#1f3b64}
|
||
.fin-result-card{background:linear-gradient(165deg,#f0f7ff 0%,#e8f4fc 100%);border:1px solid #c7d9ee;border-radius:12px;padding:14px;box-shadow:inset 0 1px 0 rgba(255,255,255,.6)}
|
||
body.dark .fin-result-card{background:linear-gradient(165deg,#172554 0%,#1e3a5f 100%);border-color:#334e80}
|
||
.fin-row{display:grid;grid-template-columns:120px 1fr;gap:8px 12px;align-items:center;margin-bottom:10px;font-size:13px}
|
||
@media (max-width:520px){.fin-row{grid-template-columns:1fr}}
|
||
.fin-row label{color:var(--muted)}
|
||
.fin-muted{font-size:12px;color:var(--muted);margin-top:4px}
|
||
.fin-out-num{font-size:22px;font-weight:700;color:#1f3b64;letter-spacing:.02em}
|
||
body.dark .fin-out-num{color:#e2e8f0}
|
||
.fin-cn-cap{font-size:13px;color:#475569;margin-top:6px;line-height:1.5}
|
||
.fin-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||
.tree-link.ancestor{font-weight:600;color:#334155}
|
||
.tree-actions{display:inline-flex;gap:4px;margin-left:auto;opacity:.25;transition:opacity .2s}
|
||
.tree-item:hover .tree-actions,.tree-link.current + .tree-actions{opacity:1}
|
||
.tree-btn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border:1px solid transparent;border-radius:5px;text-decoration:none;font-size:11px;background:transparent;color:var(--muted)}
|
||
.tree-btn:hover{border-color:var(--input-border);background:var(--input-bg)}
|
||
.tree-btn.edit:hover{color:#1d4ed8}
|
||
.tree-btn.add:hover{color:#0f766e}
|
||
.tree-btn.del{opacity:0;visibility:hidden;transition:opacity .18s}
|
||
.tree-item:hover .tree-btn.del,.tree-link.current + .tree-actions .tree-btn.del{opacity:1;visibility:visible}
|
||
.tree-btn.del:hover{color:#b91c1c;border-color:#fecaca;background:#fff1f2}
|
||
.pager{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-top:12px;flex-wrap:wrap}
|
||
.pager a{color:#1d4ed8;text-decoration:none}
|
||
.pager a:hover{text-decoration:underline}
|
||
.theme-btn{background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.32)}
|
||
.theme-btn:hover{background:rgba(255,255,255,.26)}
|
||
.icon-actions{display:flex;gap:4px;flex-wrap:nowrap;white-space:nowrap}
|
||
.icon-btn.danger{border-color:#fecaca;color:#b91c1c}
|
||
.icon-btn.danger:hover{background:#fee2e2;border-color:#fecaca;color:#b91c1c}
|
||
.clear-link{padding:8px 10px;border:1px solid var(--input-border);border-radius:8px;text-decoration:none;color:var(--muted);font-size:13px}
|
||
.clear-link:hover{border-color:#2f5da8;color:#1d4ed8}
|
||
@media (max-width:1280px){
|
||
.grid{grid-template-columns:minmax(170px,220px) minmax(0,1fr)}
|
||
.tree-link{max-width:130px}
|
||
}
|
||
@media (max-width:1024px){.grid{grid-template-columns:1fr}.tree{position:static;max-height:none}.metric-wrap{grid-template-columns:repeat(3,minmax(120px,1fr))}.top{align-items:flex-start}.wrap{padding:10px 12px 4px}}
|
||
@media (max-width:720px){.metric-wrap{grid-template-columns:repeat(2,minmax(120px,1fr))}}
|
||
@media (max-width:780px){
|
||
.archive-form-grid .form-row-2,.archive-form-grid .form-row-3,.archive-form-grid .form-row-overdue{grid-template-columns:1fr}
|
||
}
|
||
@media (max-width:1366px){
|
||
th,td{padding:6px 5px;font-size:12px}
|
||
.col-name{width:100px}.col-inst{width:90px}.col-borrower{width:82px}.col-guarantor{width:78px}.col-amount{width:76px}
|
||
.col-date{width:64px}.col-rate{width:42px}.col-status{width:48px}.col-action{width:126px}
|
||
.icon-btn,.toolbar-btn{height:26px;padding:0 8px;font-size:11px}
|
||
}
|
||
@media (max-width:1180px){
|
||
.archive-table .hide-md{display:none}
|
||
.col-name{width:140px}.col-inst{width:110px}.col-borrower{width:100px}.col-guarantor{width:92px}.col-action{width:132px}
|
||
}
|
||
@media (max-width:900px){
|
||
.archive-table thead{display:none}
|
||
.archive-table,.archive-table tbody,.archive-table tr,.archive-table td{display:block;width:100%}
|
||
.archive-table tr{border:1px solid var(--line);border-radius:10px;padding:8px;margin-bottom:8px;background:var(--card)}
|
||
.archive-table td{border:0;padding:4px 0}
|
||
.archive-table td::before{
|
||
content:attr(data-label);
|
||
display:inline-block;
|
||
min-width:84px;
|
||
color:var(--muted);
|
||
font-size:12px;
|
||
margin-right:6px;
|
||
}
|
||
.archive-table td.col-check::before{content:"选择"}
|
||
.archive-table td.col-kind::before{content:"类型"}
|
||
.archive-table td.col-check{padding-bottom:6px;border-bottom:1px dashed var(--line);margin-bottom:6px}
|
||
.archive-table td.col-action{position:static;background:transparent;width:100%;padding-top:8px;border-top:1px dashed var(--line);margin-top:6px}
|
||
.archive-table td.col-action::before{content:"操作"}
|
||
.archive-table .hide-md{display:block}
|
||
.icon-actions{flex-wrap:wrap;white-space:normal}
|
||
}
|
||
</style>
|
||
</head><body>
|
||
<div class="top">
|
||
<div class="brand">金牛集团贷款档案管理系统</div>
|
||
<div style="display:flex;align-items:center;flex-wrap:wrap;row-gap:6px">
|
||
<span><?= e(maskUsername((string)$me['username'])) ?> (<?= e(roleLabel((string)$me['role'])) ?>)</span>
|
||
<button type="button" id="theme_toggle_btn" class="a theme-btn" onclick="toggleTheme()">🌙</button>
|
||
<a class="a" href="<?= e($lastListUrl) ?>" onclick="if(window.history.length>1){window.history.back();return false;}">返回上页</a>
|
||
<a class="a" href="index.php">返回列表</a>
|
||
<a class="a" href="index.php?action=profile">个人设置</a>
|
||
<?php if ($me['role'] === 'admin'): ?><a class="a" href="index.php?action=users">用户管理</a><?php endif; ?>
|
||
<?php if ($me['role'] === 'admin'): ?><a class="a" href="index.php?action=delete_approvals">删除审核</a><?php endif; ?>
|
||
<a class="a" href="index.php?action=logout">退出</a>
|
||
</div>
|
||
</div>
|
||
<div class="wrap">
|
||
<?php if ($flash): ?>
|
||
<div id="flash-banner" class="<?= $flash['type'] === 'ok' ? 'flash-ok' : 'flash-err' ?>"><?= e($flash['msg']) ?></div>
|
||
<script>
|
||
window.addEventListener('load', function () {
|
||
var el = document.getElementById('flash-banner');
|
||
if (el) {
|
||
el.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||
setTimeout(function () {
|
||
el.style.transition = 'opacity 0.5s ease, transform 0.5s ease, max-height 0.5s ease, margin 0.5s ease, padding 0.5s ease';
|
||
el.style.opacity = '0';
|
||
el.style.transform = 'translateY(-6px)';
|
||
el.style.maxHeight = '0';
|
||
el.style.margin = '0';
|
||
el.style.paddingTop = '0';
|
||
el.style.paddingBottom = '0';
|
||
el.style.overflow = 'hidden';
|
||
}, 3000);
|
||
}
|
||
});
|
||
</script>
|
||
<?php endif; ?>
|
||
<?php
|
||
}
|
||
|
||
function pageFooter(): void
|
||
{
|
||
echo <<<HTML
|
||
</div>
|
||
<script>
|
||
(function(){
|
||
var saved=localStorage.getItem('theme_mode');
|
||
if(saved==='dark'){document.body.classList.add('dark');}
|
||
refreshThemeIcon();
|
||
})();
|
||
function refreshThemeIcon(){
|
||
var btn=document.getElementById('theme_toggle_btn');
|
||
if(!btn){return;}
|
||
btn.textContent=document.body.classList.contains('dark')?'☀':'🌙';
|
||
btn.title=document.body.classList.contains('dark')?'切换浅色模式':'切换深色模式';
|
||
}
|
||
function toggleTheme(){
|
||
document.body.classList.toggle('dark');
|
||
localStorage.setItem('theme_mode', document.body.classList.contains('dark') ? 'dark' : 'light');
|
||
refreshThemeIcon();
|
||
}
|
||
</script>
|
||
</body></html>
|
||
HTML;
|
||
}
|
||
|
||
function fetchDirectories(PDO $pdo): array
|
||
{
|
||
return $pdo->query('SELECT * FROM directories ORDER BY level ASC, id ASC')->fetchAll();
|
||
}
|
||
|
||
function safePathPart(string $name): string
|
||
{
|
||
$name = trim($name);
|
||
$name = preg_replace('/[\/\\\\:\*\?"<>\|]+/u', '_', $name);
|
||
$name = preg_replace('/\s+/u', '_', (string)$name);
|
||
$name = trim((string)$name, '._');
|
||
return $name !== '' ? $name : 'unnamed';
|
||
}
|
||
|
||
function buildArchiveUploadSubdir(PDO $pdo, int $archiveId): string
|
||
{
|
||
$stmt = $pdo->prepare('SELECT id,name,directory_id FROM archives WHERE id=:id');
|
||
$stmt->execute([':id' => $archiveId]);
|
||
$archive = $stmt->fetch();
|
||
if (!$archive) {
|
||
return 'unknown/archive_' . $archiveId;
|
||
}
|
||
$dirs = fetchDirectories($pdo);
|
||
$map = [];
|
||
foreach ($dirs as $d) {
|
||
$map[(int)$d['id']] = $d;
|
||
}
|
||
$parts = [];
|
||
$cur = (int)$archive['directory_id'];
|
||
while (isset($map[$cur])) {
|
||
$parts[] = safePathPart((string)$map[$cur]['name']);
|
||
$pid = $map[$cur]['parent_id'];
|
||
if ($pid === null) {
|
||
break;
|
||
}
|
||
$cur = (int)$pid;
|
||
}
|
||
$parts = array_reverse($parts);
|
||
$parts[] = 'archive_' . (int)$archive['id'] . '_' . safePathPart((string)$archive['name']);
|
||
return implode('/', $parts);
|
||
}
|
||
|
||
function maskUsername(string $username): string
|
||
{
|
||
$len = mb_strlen($username);
|
||
if ($len <= 2) {
|
||
return $username;
|
||
}
|
||
return mb_substr($username, 0, 1) . '***' . mb_substr($username, -1);
|
||
}
|
||
|
||
function roleLabel(string $role): string
|
||
{
|
||
return $role === 'admin' ? '管理员' : '普通用户';
|
||
}
|
||
|
||
function repaymentPlanStorePath(): string
|
||
{
|
||
return __DIR__ . '/../data/repayment_plans.json';
|
||
}
|
||
|
||
function loadRepaymentPlans(): array
|
||
{
|
||
$path = repaymentPlanStorePath();
|
||
if (!is_file($path)) {
|
||
return [];
|
||
}
|
||
$raw = file_get_contents($path);
|
||
if ($raw === false || $raw === '') {
|
||
return [];
|
||
}
|
||
$data = json_decode($raw, true);
|
||
return is_array($data) ? $data : [];
|
||
}
|
||
|
||
function saveRepaymentPlans(array $data): void
|
||
{
|
||
$path = repaymentPlanStorePath();
|
||
$dir = dirname($path);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0775, true);
|
||
}
|
||
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
|
||
}
|
||
|
||
function getArchiveRepaymentPlan(array $all, int $archiveId): array
|
||
{
|
||
return $all[(string)$archiveId] ?? ['enabled' => false, 'plans' => []];
|
||
}
|
||
|
||
function financeStorePath(): string
|
||
{
|
||
return __DIR__ . '/../data/archive_finance.json';
|
||
}
|
||
|
||
function loadFinanceConfigs(): array
|
||
{
|
||
$path = financeStorePath();
|
||
if (!is_file($path)) {
|
||
return [];
|
||
}
|
||
$raw = file_get_contents($path);
|
||
if ($raw === false || $raw === '') {
|
||
return [];
|
||
}
|
||
$data = json_decode($raw, true);
|
||
return is_array($data) ? $data : [];
|
||
}
|
||
|
||
function saveFinanceConfigs(array $data): void
|
||
{
|
||
$path = financeStorePath();
|
||
$dir = dirname($path);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0775, true);
|
||
}
|
||
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
|
||
}
|
||
|
||
function getArchiveFinanceConfig(array $all, int $archiveId): array
|
||
{
|
||
$cfg = $all[(string)$archiveId] ?? [];
|
||
return [
|
||
'interest_mode' => $cfg['interest_mode'] ?? 'monthly',
|
||
'settlement_rule' => $cfg['settlement_rule'] ?? 'monthly_day',
|
||
'settlement_day' => (int)($cfg['settlement_day'] ?? 21),
|
||
'overdue_float_pct' => (float)($cfg['overdue_float_pct'] ?? 50),
|
||
'manual_overdue' => !empty($cfg['manual_overdue']),
|
||
'manual_early_settled' => !empty($cfg['manual_early_settled']),
|
||
'remark' => (string)($cfg['remark'] ?? ''),
|
||
'manual_principal_repaid' => (float)($cfg['manual_principal_repaid'] ?? 0),
|
||
'manual_interest_paid' => (float)($cfg['manual_interest_paid'] ?? 0),
|
||
];
|
||
}
|
||
|
||
/** 计息用本金:优先剩余额度,否则授信额度 */
|
||
function effectivePrincipalForInterest(array $archive): float
|
||
{
|
||
if (isset($archive['remaining_limit']) && $archive['remaining_limit'] !== null && $archive['remaining_limit'] !== '') {
|
||
return max(0.0, (float)$archive['remaining_limit']);
|
||
}
|
||
if ($archive['credit_limit'] === null || $archive['credit_limit'] === '') {
|
||
return 0.0;
|
||
}
|
||
return max(0.0, (float)$archive['credit_limit']);
|
||
}
|
||
|
||
function archiveStatusWithConfig(?string $endDate, array $financeCfg): string
|
||
{
|
||
if (!empty($financeCfg['manual_early_settled'])) {
|
||
return '已完成';
|
||
}
|
||
if (!empty($financeCfg['manual_overdue'])) {
|
||
return '逾期中';
|
||
}
|
||
// 与 helpers::archiveStatus 一致;内联避免因 helpers 未同步部署仍按旧逻辑显示「到期自动已完成」
|
||
if (!$endDate) {
|
||
return '待更新';
|
||
}
|
||
$today = date('Y-m-d');
|
||
if ($endDate >= $today) {
|
||
return '履行中';
|
||
}
|
||
return '待更新';
|
||
}
|
||
|
||
/** 便捷查询汇总:履行中与逾期中均视为未结清存续业务 */
|
||
function archiveStatusIsOngoingAggregate(?string $endDate, array $financeCfg): bool
|
||
{
|
||
$s = archiveStatusWithConfig($endDate, $financeCfg);
|
||
return $s === '履行中' || $s === '逾期中';
|
||
}
|
||
|
||
/** 与当前结息周期对齐的区间起点(按 refDate 所在周期),用于「当前应付利息」 */
|
||
function interestPeriodStartForRefDate(array $archive, array $financeCfg, string $refDate): string
|
||
{
|
||
$startDate = $archive['start_date'] ?: $refDate;
|
||
$mode = (string)($financeCfg['interest_mode'] ?? 'monthly');
|
||
$settlementRule = (string)($financeCfg['settlement_rule'] ?? 'monthly_day');
|
||
$day = max(1, min(28, (int)($financeCfg['settlement_day'] ?? 20)));
|
||
$periodStart = $startDate;
|
||
if ($settlementRule === 'monthly_day' && $mode !== 'bullet') {
|
||
$curYm = date('Y-m', strtotime($refDate));
|
||
$thisSettle = $curYm . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
|
||
if ($refDate >= $thisSettle) {
|
||
$periodStart = $thisSettle;
|
||
} else {
|
||
$periodStart = date('Y-m-d', strtotime('-1 month', strtotime($thisSettle)));
|
||
}
|
||
} elseif ($settlementRule === 'quarterly_day' && $mode !== 'bullet') {
|
||
$m = (int)date('n', strtotime($refDate));
|
||
$qStartMonth = ((int)(($m - 1) / 3)) * 3 + 1;
|
||
$curQuarterSettle = date('Y', strtotime($refDate)) . '-' . str_pad((string)$qStartMonth, 2, '0', STR_PAD_LEFT) . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
|
||
if ($refDate >= $curQuarterSettle) {
|
||
$periodStart = $curQuarterSettle;
|
||
} else {
|
||
$periodStart = date('Y-m-d', strtotime('-3 month', strtotime($curQuarterSettle)));
|
||
}
|
||
}
|
||
if ($periodStart < $startDate) {
|
||
$periodStart = $startDate;
|
||
}
|
||
return $periodStart;
|
||
}
|
||
|
||
/**
|
||
* 下一结息日所在「整段结息周期」的起始日:上一结息日与起息日孰晚。
|
||
* 用于结息日应付利息(自周期起点至结息日,而非自今日至结息日)。
|
||
*/
|
||
function settlementPeriodStartForNextSettle(string $nextSettle, array $archive, array $financeCfg): string
|
||
{
|
||
$startDate = $archive['start_date'] ?: $nextSettle;
|
||
$mode = (string)($financeCfg['interest_mode'] ?? 'monthly');
|
||
$settlementRule = (string)($financeCfg['settlement_rule'] ?? 'monthly_day');
|
||
$day = max(1, min(28, (int)($financeCfg['settlement_day'] ?? 20)));
|
||
if ($settlementRule === 'maturity_once' || $mode === 'bullet') {
|
||
return $startDate;
|
||
}
|
||
$prevAnchor = $startDate;
|
||
if ($settlementRule === 'monthly_day') {
|
||
$prevAnchor = date('Y-m-d', strtotime('-1 month', strtotime($nextSettle)));
|
||
} elseif ($settlementRule === 'quarterly_day') {
|
||
$prevAnchor = date('Y-m-d', strtotime('-3 months', strtotime($nextSettle)));
|
||
}
|
||
return max($startDate, $prevAnchor);
|
||
}
|
||
|
||
/** 自 periodStart 起至 throughDate(含区间逻辑与 calcCurrentInterest 一致)的应计利息,不含「已归还利息」扣减 */
|
||
function calcAccruedInterestForRange(array $archive, array $financeCfg, array $repaymentPlans, string $periodStart, string $throughDate): float
|
||
{
|
||
$principal = effectivePrincipalForInterest($archive);
|
||
$annualRate = $archive['annual_rate'] === null ? 0.0 : (float)$archive['annual_rate'];
|
||
if ($principal <= 0 || $annualRate <= 0 || $throughDate < $periodStart) {
|
||
return 0.0;
|
||
}
|
||
|
||
$status = archiveStatusWithConfig($archive['end_date'], $financeCfg);
|
||
$overdueRate = $annualRate * (1 + ((float)($financeCfg['overdue_float_pct'] ?? 0) / 100));
|
||
$events = [];
|
||
$overduePrincipal = 0.0;
|
||
foreach ($repaymentPlans as $p) {
|
||
$amount = (float)($p['amount'] ?? 0);
|
||
if ($amount <= 0) {
|
||
continue;
|
||
}
|
||
$pStatus = (string)($p['status'] ?? 'pending');
|
||
if ($pStatus === 'paid' && !empty($p['paid_at'])) {
|
||
$paidAt = (string)$p['paid_at'];
|
||
if ($paidAt >= $periodStart && $paidAt <= $throughDate) {
|
||
$events[] = ['date' => $paidAt, 'type' => 'paid', 'amount' => $amount];
|
||
}
|
||
continue;
|
||
}
|
||
if ($pStatus === 'overdue') {
|
||
$dueDate = (string)($p['due_date'] ?? '');
|
||
if ($dueDate === '' || $dueDate > $throughDate) {
|
||
continue;
|
||
}
|
||
if ($dueDate < $periodStart) {
|
||
$overduePrincipal += $amount;
|
||
} else {
|
||
$events[] = ['date' => $dueDate, 'type' => 'overdue_start', 'amount' => $amount];
|
||
}
|
||
}
|
||
}
|
||
usort($events, function ($a, $b) {
|
||
$cmp = strcmp((string)$a['date'], (string)$b['date']);
|
||
if ($cmp !== 0) {
|
||
return $cmp;
|
||
}
|
||
$orderA = ($a['type'] ?? '') === 'overdue_start' ? 0 : 1;
|
||
$orderB = ($b['type'] ?? '') === 'overdue_start' ? 0 : 1;
|
||
return $orderA <=> $orderB;
|
||
});
|
||
|
||
$interest = 0.0;
|
||
$cursor = $periodStart;
|
||
$currentPrincipal = $principal;
|
||
foreach ($events as $ev) {
|
||
$evDate = (string)($ev['date'] ?? '');
|
||
if ($evDate > $cursor) {
|
||
$days = (strtotime($evDate) - strtotime($cursor)) / 86400;
|
||
if ($days > 0) {
|
||
$normalPrincipal = max(0, $currentPrincipal - $overduePrincipal);
|
||
$interest += $normalPrincipal * ($annualRate / 100) * ($days / 360);
|
||
$interest += $overduePrincipal * ($overdueRate / 100) * ($days / 360);
|
||
}
|
||
}
|
||
$amount = (float)($ev['amount'] ?? 0);
|
||
if (($ev['type'] ?? '') === 'overdue_start') {
|
||
$overduePrincipal = min($currentPrincipal, $overduePrincipal + $amount);
|
||
} elseif (($ev['type'] ?? '') === 'paid') {
|
||
$currentPrincipal = max(0, $currentPrincipal - $amount);
|
||
$overduePrincipal = min($overduePrincipal, $currentPrincipal);
|
||
}
|
||
$cursor = $evDate;
|
||
}
|
||
if ($throughDate > $cursor) {
|
||
$days = (strtotime($throughDate) - strtotime($cursor)) / 86400;
|
||
if ($days > 0) {
|
||
$normalPrincipal = max(0, $currentPrincipal - $overduePrincipal);
|
||
if ($overduePrincipal <= 0 && $status === '逾期中') {
|
||
$interest += $currentPrincipal * ($overdueRate / 100) * ($days / 360);
|
||
} else {
|
||
$interest += $normalPrincipal * ($annualRate / 100) * ($days / 360);
|
||
$interest += $overduePrincipal * ($overdueRate / 100) * ($days / 360);
|
||
}
|
||
}
|
||
}
|
||
return round($interest, 2);
|
||
}
|
||
|
||
function applyInterestPaidCredit(float $accrued, array $financeCfg): float
|
||
{
|
||
$paid = (float)($financeCfg['manual_interest_paid'] ?? 0);
|
||
return round(max(0.0, $accrued - $paid), 2);
|
||
}
|
||
|
||
function calcCurrentInterest(array $archive, array $financeCfg, array $repaymentPlans): float
|
||
{
|
||
$today = date('Y-m-d');
|
||
$periodStart = interestPeriodStartForRefDate($archive, $financeCfg, $today);
|
||
$raw = calcAccruedInterestForRange($archive, $financeCfg, $repaymentPlans, $periodStart, $today);
|
||
return applyInterestPaidCredit($raw, $financeCfg);
|
||
}
|
||
|
||
/** 最近一个结息日应付的整周期利息(与利息查询一致) */
|
||
function calcSettlementPeriodInterest(array $archive, array $financeCfg, array $repaymentPlans, string $todayRef): float
|
||
{
|
||
$mode = (string)($financeCfg['interest_mode'] ?? 'monthly');
|
||
$annualRate = $archive['annual_rate'] === null ? 0.0 : (float)$archive['annual_rate'];
|
||
if (effectivePrincipalForInterest($archive) <= 0 || $annualRate <= 0) {
|
||
return 0.0;
|
||
}
|
||
$nextSettle = $mode === 'bullet'
|
||
? (((string)($archive['end_date'] ?? '') !== '' && (string)$archive['end_date'] >= $todayRef) ? (string)$archive['end_date'] : null)
|
||
: nextSettlementDate($todayRef, $mode, (int)($financeCfg['settlement_day'] ?? 21));
|
||
if ($nextSettle === null || $nextSettle === '') {
|
||
return 0.0;
|
||
}
|
||
$periodStart = settlementPeriodStartForNextSettle($nextSettle, $archive, $financeCfg);
|
||
$raw = calcAccruedInterestForRange($archive, $financeCfg, $repaymentPlans, $periodStart, $nextSettle);
|
||
return applyInterestPaidCredit($raw, $financeCfg);
|
||
}
|
||
|
||
function recalcRemainingLimitByPlans(PDO $pdo, int $archiveId, array $plans): void
|
||
{
|
||
if ($archiveId <= 0) {
|
||
return;
|
||
}
|
||
$st = $pdo->prepare('SELECT credit_limit FROM archives WHERE id=:id');
|
||
$st->execute([':id' => $archiveId]);
|
||
$credit = $st->fetchColumn();
|
||
if ($credit === false || $credit === null) {
|
||
return;
|
||
}
|
||
$paidSum = 0.0;
|
||
foreach ($plans as $p) {
|
||
if ((string)($p['status'] ?? '') === 'paid') {
|
||
$paidSum += (float)($p['amount'] ?? 0);
|
||
}
|
||
}
|
||
$financeAll = loadFinanceConfigs();
|
||
$manualPrincipal = (float)(getArchiveFinanceConfig($financeAll, $archiveId)['manual_principal_repaid'] ?? 0);
|
||
$remaining = max(0, (float)$credit - $paidSum - $manualPrincipal);
|
||
$pdo->prepare('UPDATE archives SET remaining_limit=:r,updated_at=:u WHERE id=:id')
|
||
->execute([':r' => $remaining, ':u' => now(), ':id' => $archiveId]);
|
||
}
|
||
|
||
function uploadArchiveFiles(PDO $pdo, int $archiveId, array $files): void
|
||
{
|
||
if ($archiveId <= 0 || empty($files) || empty($files['name']) || !is_array($files['name'])) {
|
||
return;
|
||
}
|
||
$names = $files['name'];
|
||
$tmpNames = $files['tmp_name'] ?? [];
|
||
$sizes = $files['size'] ?? [];
|
||
$errors = $files['error'] ?? [];
|
||
|
||
$subDir = buildArchiveUploadSubdir($pdo, $archiveId);
|
||
$targetDir = rtrim(UPLOAD_DIR . '/' . $subDir, '/');
|
||
if (!is_dir($targetDir)) {
|
||
mkdir($targetDir, 0775, true);
|
||
}
|
||
for ($i = 0; $i < count($names); $i++) {
|
||
if ((int)($errors[$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||
continue;
|
||
}
|
||
if ((int)($sizes[$i] ?? 0) > MAX_FILE_SIZE) {
|
||
continue;
|
||
}
|
||
$original = (string)$names[$i];
|
||
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
|
||
if (!in_array($ext, ALLOWED_EXTENSIONS, true)) {
|
||
continue;
|
||
}
|
||
$normalized = normalizeFileName($original);
|
||
$stored = $subDir . '/' . uniqid('f_', true) . '_' . $normalized;
|
||
if (move_uploaded_file((string)$tmpNames[$i], UPLOAD_DIR . '/' . $stored)) {
|
||
$pdo->prepare('INSERT INTO files(archive_id,original_name,stored_name,file_ext,file_size,created_at) VALUES(:a,:o,:s,:e,:z,:c)')
|
||
->execute([':a' => $archiveId, ':o' => $original, ':s' => $stored, ':e' => $ext, ':z' => (int)$sizes[$i], ':c' => now()]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function deleteApprovalStorePath(): string
|
||
{
|
||
return __DIR__ . '/../data/delete_approvals.json';
|
||
}
|
||
|
||
function loadDeleteApprovals(): array
|
||
{
|
||
$path = deleteApprovalStorePath();
|
||
if (!is_file($path)) {
|
||
return [];
|
||
}
|
||
$raw = file_get_contents($path);
|
||
if ($raw === false || $raw === '') {
|
||
return [];
|
||
}
|
||
$data = json_decode($raw, true);
|
||
return is_array($data) ? $data : [];
|
||
}
|
||
|
||
function saveDeleteApprovals(array $data): void
|
||
{
|
||
$path = deleteApprovalStorePath();
|
||
$dir = dirname($path);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0775, true);
|
||
}
|
||
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
|
||
}
|
||
|
||
function currentUrlFromServer(): string
|
||
{
|
||
$uri = (string)($_SERVER['REQUEST_URI'] ?? '');
|
||
if ($uri === '' || strpos($uri, 'index.php') === false) {
|
||
return 'index.php';
|
||
}
|
||
return $uri;
|
||
}
|
||
|
||
function redirectBackOr(string $fallback): void
|
||
{
|
||
$posted = trim((string)($_POST['redirect_to'] ?? ''));
|
||
$query = trim((string)($_GET['redirect_to'] ?? ''));
|
||
$ref = trim((string)($_SERVER['HTTP_REFERER'] ?? ''));
|
||
$target = $posted !== '' ? $posted : ($query !== '' ? $query : $ref);
|
||
if ($target === '' || strpos($target, 'index.php') === false) {
|
||
$target = $fallback;
|
||
}
|
||
header('Location: ' . $target);
|
||
exit;
|
||
}
|
||
|
||
function fetchArchiveSimpleMap(PDO $pdo): array
|
||
{
|
||
$rows = $pdo->query('SELECT id,name,loan_institution,borrower,archive_kind FROM archives')->fetchAll();
|
||
$map = [];
|
||
foreach ($rows as $r) {
|
||
$id = (int)($r['id'] ?? 0);
|
||
if ($id <= 0) {
|
||
continue;
|
||
}
|
||
$map[$id] = [
|
||
'name' => (string)($r['name'] ?? ''),
|
||
'loan_institution' => (string)($r['loan_institution'] ?? ''),
|
||
'borrower' => (string)($r['borrower'] ?? ''),
|
||
'archive_kind' => normalizeArchiveKind((string)($r['archive_kind'] ?? '')),
|
||
];
|
||
}
|
||
return $map;
|
||
}
|
||
|
||
function approvalTargetLabel(array $ap, array $archiveMap): string
|
||
{
|
||
$type = (string)($ap['target_type'] ?? '');
|
||
if ($type === 'archive_batch') {
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', (array)($ap['target_ids'] ?? [])))));
|
||
$labels = [];
|
||
foreach (array_slice($ids, 0, 5) as $aid) {
|
||
if (!isset($archiveMap[$aid])) {
|
||
$labels[] = 'ID:' . $aid . '(已删除)';
|
||
continue;
|
||
}
|
||
$arc = $archiveMap[$aid];
|
||
$labels[] = '[' . archiveKindLabel((string)$arc['archive_kind']) . '] ' . (string)$arc['name'];
|
||
}
|
||
$more = count($ids) > 5 ? ' 等' . count($ids) . '条' : '';
|
||
return '批量' . (int)($ap['target_count'] ?? count($ids)) . '条:' . implode(';', $labels) . $more;
|
||
}
|
||
$aid = (int)($ap['target_id'] ?? 0);
|
||
if ($aid <= 0) {
|
||
return 'ID:0';
|
||
}
|
||
if (!isset($archiveMap[$aid])) {
|
||
return 'ID:' . $aid . '(已删除/不存在)';
|
||
}
|
||
$arc = $archiveMap[$aid];
|
||
$name = (string)$arc['name'];
|
||
$inst = (string)$arc['loan_institution'];
|
||
$borrower = (string)$arc['borrower'];
|
||
return 'ID:' . $aid . ' [' . archiveKindLabel((string)$arc['archive_kind']) . '] ' . $name . ' / ' . $inst . ' / ' . $borrower;
|
||
}
|
||
|
||
function nextSettlementDate(string $today, string $mode, int $day): ?string
|
||
{
|
||
if ($mode === 'bullet') {
|
||
return null;
|
||
}
|
||
$day = max(1, min(28, $day));
|
||
if ($mode === 'quarterly') {
|
||
$m = (int)date('n', strtotime($today));
|
||
$qStartMonth = ((int)(($m - 1) / 3)) * 3 + 1;
|
||
$cand = date('Y', strtotime($today)) . '-' . str_pad((string)$qStartMonth, 2, '0', STR_PAD_LEFT) . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
|
||
if ($cand <= $today) {
|
||
$cand = date('Y-m-d', strtotime('+3 month', strtotime($cand)));
|
||
}
|
||
return $cand;
|
||
}
|
||
$curYm = date('Y-m', strtotime($today));
|
||
$cand = $curYm . '-' . str_pad((string)$day, 2, '0', STR_PAD_LEFT);
|
||
if ($cand <= $today) {
|
||
$cand = date('Y-m-d', strtotime('+1 month', strtotime($cand)));
|
||
}
|
||
return $cand;
|
||
}
|
||
|
||
function ensureDirectoryPath(PDO $pdo, string $lv1, string $lv2, string $lv3): int
|
||
{
|
||
$clean = function (string $v): string {
|
||
$v = str_replace("\xC2\xA0", ' ', $v);
|
||
$v = preg_replace('/\s+/u', ' ', trim($v));
|
||
return (string)$v;
|
||
};
|
||
$lv1 = $clean($lv1);
|
||
$lv2 = $clean($lv2);
|
||
$lv3 = $clean($lv3);
|
||
|
||
$findOrCreate = function (?int $parentId, int $level, string $name) use ($pdo): int {
|
||
if ($parentId === null) {
|
||
$q = $pdo->prepare('SELECT id FROM directories WHERE name=:n AND level=:l AND parent_id IS NULL');
|
||
$q->execute([':n' => $name, ':l' => $level]);
|
||
} else {
|
||
$q = $pdo->prepare('SELECT id FROM directories WHERE name=:n AND level=:l AND parent_id=:p');
|
||
$q->execute([':n' => $name, ':l' => $level, ':p' => $parentId]);
|
||
}
|
||
$id = (int)$q->fetchColumn();
|
||
if ($id > 0) {
|
||
return $id;
|
||
}
|
||
$ins = $pdo->prepare('INSERT INTO directories(name,parent_id,level,created_at) VALUES(:n,:p,:l,:c)');
|
||
$ins->bindValue(':n', $name);
|
||
$ins->bindValue(':p', $parentId, $parentId === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
|
||
$ins->bindValue(':l', $level, PDO::PARAM_INT);
|
||
$ins->bindValue(':c', now());
|
||
$ins->execute();
|
||
return (int)$pdo->lastInsertId();
|
||
};
|
||
$id1 = $findOrCreate(null, 1, $lv1);
|
||
$id2 = $findOrCreate($id1, 2, $lv2);
|
||
return $findOrCreate($id2, 3, $lv3);
|
||
}
|
||
|
||
function hardDeleteArchive(PDO $pdo, int $archiveId): void
|
||
{
|
||
$stmt = $pdo->prepare('SELECT stored_name FROM files WHERE archive_id=:a');
|
||
$stmt->execute([':a' => $archiveId]);
|
||
foreach ($stmt->fetchAll() as $f) {
|
||
$filePath = UPLOAD_DIR . '/' . $f['stored_name'];
|
||
if (is_file($filePath)) {
|
||
unlink($filePath);
|
||
}
|
||
}
|
||
$pdo->prepare('DELETE FROM archives WHERE id=:id')->execute([':id' => $archiveId]);
|
||
}
|
||
|
||
function loanInstitutionStorePath(): string
|
||
{
|
||
return __DIR__ . '/../data/loan_institutions.json';
|
||
}
|
||
|
||
function defaultLoanInstitutions(): array
|
||
{
|
||
return [
|
||
'河南农商银行登封支行',
|
||
'郑州银行登封支行',
|
||
'浦发银行郑州金水支行',
|
||
'中国银行登封支行',
|
||
'建设银行登封支行',
|
||
'邮储银行登封支行',
|
||
'河南农商银行郑州关虎屯支行',
|
||
'中信银行登封支行',
|
||
];
|
||
}
|
||
|
||
function loadLoanInstitutions(): array
|
||
{
|
||
$path = loanInstitutionStorePath();
|
||
if (!is_file($path)) {
|
||
return defaultLoanInstitutions();
|
||
}
|
||
$raw = file_get_contents($path);
|
||
if ($raw === false || $raw === '') {
|
||
return defaultLoanInstitutions();
|
||
}
|
||
$arr = json_decode($raw, true);
|
||
if (!is_array($arr)) {
|
||
return defaultLoanInstitutions();
|
||
}
|
||
$out = [];
|
||
foreach ($arr as $x) {
|
||
$v = trim((string)$x);
|
||
if ($v !== '') {
|
||
$out[] = $v;
|
||
}
|
||
}
|
||
return $out ?: defaultLoanInstitutions();
|
||
}
|
||
|
||
function saveLoanInstitutions(array $items): void
|
||
{
|
||
$out = [];
|
||
foreach ($items as $x) {
|
||
$v = trim((string)$x);
|
||
if ($v !== '') {
|
||
$out[] = $v;
|
||
}
|
||
}
|
||
$out = array_values(array_unique($out));
|
||
if (empty($out)) {
|
||
$out = defaultLoanInstitutions();
|
||
}
|
||
$path = loanInstitutionStorePath();
|
||
$dir = dirname($path);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0775, true);
|
||
}
|
||
file_put_contents($path, json_encode($out, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
|
||
}
|
||
|
||
function autoArchiveName(string $borrower, ?float $creditLimit): string
|
||
{
|
||
$b = trim($borrower);
|
||
if ($b === '') {
|
||
return '';
|
||
}
|
||
$wan = $creditLimit === null ? '' : rtrim(rtrim(number_format($creditLimit / 10000, 2, '.', ''), '0'), '.');
|
||
return $wan === '' ? $b : ($b . $wan . '万元');
|
||
}
|
||
|
||
/** 对外担保:担保人 +「对外担保」+ 授信/10000 +「万元」 */
|
||
function autoExternalGuaranteeArchiveName(string $guarantor, ?float $creditLimit): string
|
||
{
|
||
$g = trim($guarantor);
|
||
if ($g === '') {
|
||
return '';
|
||
}
|
||
$wan = $creditLimit === null ? '' : rtrim(rtrim(number_format($creditLimit / 10000, 2, '.', ''), '0'), '.');
|
||
return $wan === '' ? '' : ($g . '对外担保' . $wan . '万元');
|
||
}
|
||
|
||
function ensureAutoBankLoanDirectory(PDO $pdo, ?string $startDate, string $loanInstitution): int
|
||
{
|
||
$year = $startDate ? date('Y', strtotime((string) $startDate)) : '';
|
||
$bank = trim($loanInstitution);
|
||
if ($year === '' || $bank === '') {
|
||
return 0;
|
||
}
|
||
return ensureDirectoryPath($pdo, '银行贷款', $year, $bank);
|
||
}
|
||
|
||
function ensureAutoExternalGuaranteeDirectory(PDO $pdo, ?string $startDate, string $guarantor): int
|
||
{
|
||
$year = $startDate ? date('Y', strtotime((string) $startDate)) : '';
|
||
$g = trim($guarantor);
|
||
if ($year === '' || $g === '') {
|
||
return 0;
|
||
}
|
||
return ensureDirectoryPath($pdo, '对外担保', $year, $g);
|
||
}
|
||
|
||
function collectAncestorIds(array $dirs, ?int $selectedId): array
|
||
{
|
||
if (!$selectedId) {
|
||
return [];
|
||
}
|
||
$parentMap = [];
|
||
foreach ($dirs as $d) {
|
||
$parentMap[(int)$d['id']] = $d['parent_id'] === null ? null : (int)$d['parent_id'];
|
||
}
|
||
$ids = [$selectedId];
|
||
$cur = $selectedId;
|
||
while (isset($parentMap[$cur]) && $parentMap[$cur] !== null) {
|
||
$cur = (int)$parentMap[$cur];
|
||
$ids[] = $cur;
|
||
}
|
||
return array_values(array_unique($ids));
|
||
}
|
||
|
||
/** 二级目录排序:从名称中提取年份(如 2025、2025年)升序;无年份则按名称排在后段。 */
|
||
function directoryLevel2TimeSortKey(string $name): int
|
||
{
|
||
$name = trim($name);
|
||
if ($name === '') {
|
||
return 9999;
|
||
}
|
||
if (preg_match('/^(\d{4})(?:\b|年)/u', $name, $m)) {
|
||
return (int) $m[1];
|
||
}
|
||
if (preg_match('/(\d{4})/u', $name, $m)) {
|
||
return (int) $m[1];
|
||
}
|
||
return 9999;
|
||
}
|
||
|
||
function buildTreeHtml(array $dirs, ?int $selectedId, bool $admin, array $pathIds): string
|
||
{
|
||
$byParent = [];
|
||
$dirMap = [];
|
||
foreach ($dirs as $d) {
|
||
$pid = $d['parent_id'] === null ? 0 : (int) $d['parent_id'];
|
||
$byParent[$pid][] = $d;
|
||
$dirMap[(int)$d['id']] = $d;
|
||
}
|
||
foreach ($byParent as &$children) {
|
||
if ($children === []) {
|
||
continue;
|
||
}
|
||
if ((int) ($children[0]['level'] ?? 0) === 2) {
|
||
usort($children, static function (array $a, array $b): int {
|
||
$ka = directoryLevel2TimeSortKey((string) $a['name']);
|
||
$kb = directoryLevel2TimeSortKey((string) $b['name']);
|
||
if ($ka !== $kb) {
|
||
return $ka <=> $kb;
|
||
}
|
||
return strcmp((string) $a['name'], (string) $b['name']);
|
||
});
|
||
}
|
||
}
|
||
unset($children);
|
||
$pathSet = array_flip($pathIds);
|
||
$rootNameById = [];
|
||
foreach ($dirMap as $id => $d) {
|
||
$cur = $id;
|
||
$rootName = (string)$d['name'];
|
||
while (isset($dirMap[$cur]) && $dirMap[$cur]['parent_id'] !== null) {
|
||
$cur = (int)$dirMap[$cur]['parent_id'];
|
||
if (!isset($dirMap[$cur])) {
|
||
break;
|
||
}
|
||
$rootName = (string)$dirMap[$cur]['name'];
|
||
}
|
||
$rootNameById[$id] = $rootName;
|
||
}
|
||
$render = function ($parent) use (&$render, $byParent, $selectedId, $admin, $pathSet, $rootNameById): string {
|
||
if (empty($byParent[$parent])) {
|
||
return '';
|
||
}
|
||
$html = '<ul>';
|
||
foreach ($byParent[$parent] as $d) {
|
||
$id = (int)$d['id'];
|
||
$name = e($d['name']);
|
||
$hasChildren = !empty($byParent[$id]);
|
||
$isCurrent = $selectedId === $id;
|
||
$isAncestor = isset($pathSet[$id]);
|
||
$linkClass = 'tree-link' . ($isCurrent ? ' current' : ($isAncestor ? ' ancestor' : ''));
|
||
$actions = '';
|
||
$kindQ = '';
|
||
$rootName = (string)($rootNameById[$id] ?? '');
|
||
if ($rootName === '银行贷款') {
|
||
$kindQ = '&kind=' . ARCHIVE_KIND_BANK_LOAN;
|
||
} elseif ($rootName === '对外担保') {
|
||
$kindQ = '&kind=' . ARCHIVE_KIND_EXTERNAL_GUARANTEE;
|
||
}
|
||
if ($admin) {
|
||
$actions .= '<span class="tree-actions">';
|
||
$actions .= '<a class="tree-btn edit" title="编辑目录" href="index.php?action=edit_dir&id=' . $id . '">✎</a>';
|
||
if ((int)$d['level'] < 3) {
|
||
$actions .= '<a class="tree-btn add" title="新增子目录" href="index.php?action=new_dir&parent_id=' . $id . '">+</a>';
|
||
}
|
||
$actions .= '<a class="tree-btn del" title="删除目录" href="index.php?action=delete_dir&id=' . $id . '" onclick="return confirm(\'仅空目录可删除,确认?\')">🗑</a>';
|
||
$actions .= '</span>';
|
||
}
|
||
$html .= '<li>';
|
||
if ($hasChildren) {
|
||
$open = $isAncestor ? ' open' : '';
|
||
$html .= '<details' . $open . '><summary><div class="tree-item"><span class="tree-toggle">▶</span><a class="' . $linkClass . '" href="index.php?dir_id=' . $id . $kindQ . '" title="' . $name . '">' . $name . '</a>' . $actions . '</div></summary>';
|
||
} else {
|
||
$html .= '<div class="tree-item"><span class="tree-toggle"></span><a class="' . $linkClass . '" href="index.php?dir_id=' . $id . $kindQ . '" title="' . $name . '">' . $name . '</a>' . $actions . '</div>';
|
||
}
|
||
$html .= $render($id);
|
||
if ($hasChildren) {
|
||
$html .= '</details>';
|
||
}
|
||
$html .= '</li>';
|
||
}
|
||
$html .= '</ul>';
|
||
return $html;
|
||
};
|
||
return $render(0);
|
||
}
|
||
|
||
/** 便捷查询侧栏 / 便捷查询页:折叠菜单内链接 */
|
||
function quickQueryMenuHtml(): string
|
||
{
|
||
$links = [
|
||
['query_payable_interest', '应付本息查询'],
|
||
['query_external_guarantee', '对外担保查询'],
|
||
['query_totals', '查询总金额'],
|
||
['interest_calculator', '利息计算器'],
|
||
['acceptance_discount_calculator', '承兑贴现计算器'],
|
||
['repayment_history', '历史还款记录查询'],
|
||
];
|
||
$inner = '';
|
||
foreach ($links as $pair) {
|
||
$inner .= '<a class="tree-link" href="index.php?action=' . e($pair[0]) . '">' . e($pair[1]) . '</a>';
|
||
}
|
||
return '<details class="quick-menu-details">'
|
||
. '<summary class="quick-menu-summary"><span>便捷查询</span><span class="quick-menu-hint">点击展开</span></summary>'
|
||
. '<div class="quick-links-col">' . $inner . '</div>'
|
||
. '</details>';
|
||
}
|
||
|
||
if ($action === 'new_dir') {
|
||
requireAdmin();
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$name = trim((string)($_POST['name'] ?? ''));
|
||
$parentId = (int)($_POST['parent_id'] ?? 0);
|
||
if ($name === '') {
|
||
flash('error', '目录名称不能为空');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$level = 1;
|
||
$parentValue = null;
|
||
if ($parentId > 0) {
|
||
$stmt = $pdo->prepare('SELECT * FROM directories WHERE id=:id');
|
||
$stmt->execute([':id' => $parentId]);
|
||
$p = $stmt->fetch();
|
||
if (!$p) {
|
||
flash('error', '父目录不存在');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$level = (int)$p['level'] + 1;
|
||
$parentValue = $parentId;
|
||
if ($level > 3) {
|
||
flash('error', '最多只支持三级目录');
|
||
header('Location: index.php?dir_id=' . $parentId);
|
||
exit;
|
||
}
|
||
}
|
||
$stmt = $pdo->prepare('INSERT INTO directories(name,parent_id,level,created_at) VALUES(:n,:p,:l,:c)');
|
||
$stmt->bindValue(':n', $name);
|
||
$stmt->bindValue(':p', $parentValue, $parentValue === null ? PDO::PARAM_NULL : PDO::PARAM_INT);
|
||
$stmt->bindValue(':l', $level, PDO::PARAM_INT);
|
||
$stmt->bindValue(':c', now());
|
||
$stmt->execute();
|
||
flash('ok', '目录已创建');
|
||
header('Location: index.php' . ($parentId ? '?dir_id=' . $parentId : ''));
|
||
exit;
|
||
}
|
||
$parentId = (int)($_GET['parent_id'] ?? 0);
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3>新建目录</h3>
|
||
<form method="post">
|
||
<input type="hidden" name="parent_id" value="<?= $parentId ?>">
|
||
<input name="name" placeholder="目录名称" required>
|
||
<button type="submit">保存</button>
|
||
<a href="index.php">返回</a>
|
||
</form>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'edit_dir') {
|
||
requireAdmin();
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
$stmt = $pdo->prepare('SELECT * FROM directories WHERE id=:id');
|
||
$stmt->execute([':id' => $id]);
|
||
$d = $stmt->fetch();
|
||
if (!$d) {
|
||
flash('error', '目录不存在');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$name = trim((string)($_POST['name'] ?? ''));
|
||
if ($name === '') {
|
||
flash('error', '目录名称不能为空');
|
||
} else {
|
||
$up = $pdo->prepare('UPDATE directories SET name=:n WHERE id=:id');
|
||
$up->execute([':n' => $name, ':id' => $id]);
|
||
flash('ok', '目录已更新');
|
||
header('Location: index.php?dir_id=' . $id);
|
||
exit;
|
||
}
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card"><h3>编辑目录</h3>
|
||
<form method="post">
|
||
<input name="name" value="<?= e($d['name']) ?>" required>
|
||
<button type="submit">保存</button>
|
||
<a href="index.php?dir_id=<?= $id ?>">返回</a>
|
||
</form>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'delete_dir') {
|
||
requireAdmin();
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
$c1 = $pdo->prepare('SELECT COUNT(*) FROM directories WHERE parent_id=:id');
|
||
$c1->execute([':id' => $id]);
|
||
$c2 = $pdo->prepare('SELECT COUNT(*) FROM archives WHERE directory_id=:id');
|
||
$c2->execute([':id' => $id]);
|
||
if ((int)$c1->fetchColumn() > 0 || (int)$c2->fetchColumn() > 0) {
|
||
flash('error', '目录非空,不能删除');
|
||
header('Location: index.php?dir_id=' . $id);
|
||
exit;
|
||
}
|
||
$del = $pdo->prepare('DELETE FROM directories WHERE id=:id');
|
||
$del->execute([':id' => $id]);
|
||
flash('ok', '目录已删除');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'save_archive') {
|
||
requireAdmin();
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$directoryId = (int)($_POST['directory_id'] ?? 0);
|
||
$archiveKind = normalizeArchiveKind((string)($_POST['archive_kind'] ?? ''));
|
||
$loanInstitution = trim((string)($_POST['loan_institution'] ?? ''));
|
||
$borrower = trim((string)($_POST['borrower'] ?? ''));
|
||
$guarantorEarly = trim((string)($_POST['guarantor'] ?? ''));
|
||
$creditLimitVal = ($_POST['credit_limit'] ?? '') === '' ? null : (float)$_POST['credit_limit'];
|
||
$startDateVal = $_POST['start_date'] ?: null;
|
||
if ($archiveKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
|
||
$name = autoExternalGuaranteeArchiveName($guarantorEarly, $creditLimitVal);
|
||
if ($name === '') {
|
||
flash('error', '对外担保需填写担保人、授信额度(用于自动生成档案名称)');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
} else {
|
||
$name = autoArchiveName($borrower, $creditLimitVal);
|
||
if ($name === '') {
|
||
flash('error', '借款主体与授信额度必填(用于自动生成档案名称)');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
}
|
||
if (isset($_POST['loan_institutions_json'])) {
|
||
$json = json_decode((string)$_POST['loan_institutions_json'], true);
|
||
if (is_array($json)) {
|
||
saveLoanInstitutions($json);
|
||
}
|
||
}
|
||
if ($directoryId > 0) {
|
||
$check = $pdo->prepare('SELECT level FROM directories WHERE id=:id');
|
||
$check->execute([':id' => $directoryId]);
|
||
$lv = (int)$check->fetchColumn();
|
||
if ($lv !== 3) {
|
||
flash('error', '手动选择目录时必须是三级目录');
|
||
header('Location: index.php?dir_id=' . $directoryId);
|
||
exit;
|
||
}
|
||
} else {
|
||
if ($archiveKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
|
||
$directoryId = ensureAutoExternalGuaranteeDirectory($pdo, $startDateVal, $guarantorEarly);
|
||
if ($directoryId <= 0) {
|
||
flash('error', '未选择三级目录时,对外担保需填写起始日与担保人,系统将自动创建目录:对外担保 / 年份 / 担保人');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
} else {
|
||
$directoryId = ensureAutoBankLoanDirectory($pdo, $startDateVal, $loanInstitution);
|
||
if ($directoryId <= 0) {
|
||
flash('error', '未选择三级目录时,银行贷款需填写贷款机构与起始日,系统将自动创建目录:银行贷款 / 年份 / 贷款机构');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
}
|
||
}
|
||
$planEnabled = ($_POST['plan_enabled'] ?? '0') === '1';
|
||
$allPlans = loadRepaymentPlans();
|
||
$existing = $id > 0 ? getArchiveRepaymentPlan($allPlans, $id) : ['enabled' => false, 'plans' => []];
|
||
$existingMap = [];
|
||
foreach (($existing['plans'] ?? []) as $ep) {
|
||
if (!empty($ep['id'])) {
|
||
$existingMap[(string)$ep['id']] = $ep;
|
||
}
|
||
}
|
||
$dueDates = (array)($_POST['plan_due_date'] ?? []);
|
||
$amounts = (array)($_POST['plan_amount'] ?? []);
|
||
$planIds = (array)($_POST['plan_id'] ?? []);
|
||
$planStatuses = (array)($_POST['plan_status'] ?? []);
|
||
$delayDates = (array)($_POST['plan_delay_date'] ?? []);
|
||
$plans = [];
|
||
$paidSum = 0.0;
|
||
if ($planEnabled) {
|
||
$n = max(count($dueDates), count($amounts), count($planStatuses));
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$due = trim((string)($dueDates[$i] ?? ''));
|
||
$amtRaw = trim((string)($amounts[$i] ?? ''));
|
||
if ($due === '' || $amtRaw === '') {
|
||
continue;
|
||
}
|
||
$amt = (float)$amtRaw;
|
||
if ($amt <= 0) {
|
||
continue;
|
||
}
|
||
$pid = trim((string)($planIds[$i] ?? ''));
|
||
$status = (string)($planStatuses[$i] ?? 'pending');
|
||
if (!in_array($status, ['pending', 'paid', 'delayed', 'overdue'], true)) {
|
||
$status = 'pending';
|
||
}
|
||
$today = date('Y-m-d');
|
||
if (($status === 'delayed' || $status === 'overdue') && $due > $today) {
|
||
$status = 'pending';
|
||
}
|
||
$delayDate = trim((string)($delayDates[$i] ?? ''));
|
||
if ($status === 'delayed' && $delayDate !== '') {
|
||
$due = $delayDate;
|
||
}
|
||
$old = $existingMap[$pid] ?? null;
|
||
$paidAt = null;
|
||
if ($status === 'paid') {
|
||
$paidAt = ($old && ($old['status'] ?? '') === 'paid' && !empty($old['paid_at'])) ? $old['paid_at'] : date('Y-m-d');
|
||
$paidSum += $amt;
|
||
}
|
||
$plans[] = [
|
||
'id' => $pid !== '' ? $pid : uniqid('rp_', true),
|
||
'due_date' => $due,
|
||
'amount' => $amt,
|
||
'status' => $status,
|
||
'paid_at' => $paidAt,
|
||
'updated_at' => now(),
|
||
];
|
||
}
|
||
}
|
||
|
||
$financeAllWork = loadFinanceConfigs();
|
||
$rawPrevFinance = $id > 0 ? ($financeAllWork[(string)$id] ?? []) : [];
|
||
$prevManualPrincipal = (float)($rawPrevFinance['manual_principal_repaid'] ?? 0);
|
||
$prevManualInterest = (float)($rawPrevFinance['manual_interest_paid'] ?? 0);
|
||
$addPrincipal = max(0, (float)($_POST['principal_repayment_now'] ?? 0));
|
||
$addInterest = max(0, (float)($_POST['interest_repayment_now'] ?? 0));
|
||
if ($creditLimitVal !== null) {
|
||
$repayCap = max(0, $creditLimitVal - $paidSum - $prevManualPrincipal);
|
||
if ($addPrincipal > $repayCap + 1e-6) {
|
||
flash('error', '本次归还本金超过可还金额(授信 − 分期已还 − 已累计归还本金)');
|
||
header('Location: index.php?action=archive_form' . ($id > 0 ? ('&id=' . $id) : ''));
|
||
exit;
|
||
}
|
||
}
|
||
|
||
$oldEndDate = '';
|
||
if ($id > 0) {
|
||
$stEnd = $pdo->prepare('SELECT end_date FROM archives WHERE id=:id');
|
||
$stEnd->execute([':id' => $id]);
|
||
$oldEndRow = $stEnd->fetchColumn();
|
||
if ($oldEndRow !== false && $oldEndRow !== null) {
|
||
$oldEndDate = (string)$oldEndRow;
|
||
}
|
||
}
|
||
$loanExtendTo = trim((string)($_POST['loan_extend_to'] ?? ''));
|
||
$endDatePosted = ($_POST['end_date'] ?? '') !== '' ? (string)$_POST['end_date'] : null;
|
||
$endDateFinal = $endDatePosted;
|
||
if ($loanExtendTo !== '') {
|
||
if ($startDateVal && $loanExtendTo < $startDateVal) {
|
||
flash('error', '延期后的到期日不能早于起始日');
|
||
header('Location: index.php?action=archive_form' . ($id > 0 ? ('&id=' . $id) : ''));
|
||
exit;
|
||
}
|
||
if ($oldEndDate !== '' && $loanExtendTo < $oldEndDate) {
|
||
flash('error', '延期后的到期日不能早于当前到期日');
|
||
header('Location: index.php?action=archive_form' . ($id > 0 ? ('&id=' . $id) : ''));
|
||
exit;
|
||
}
|
||
$endDateFinal = $loanExtendTo;
|
||
}
|
||
|
||
$manualPrincipalTotal = $prevManualPrincipal + $addPrincipal;
|
||
$manualInterestTotal = $prevManualInterest + $addInterest;
|
||
$remainingAuto = $creditLimitVal === null ? null : max(0, $creditLimitVal - $paidSum - $manualPrincipalTotal);
|
||
|
||
$payload = [
|
||
':name' => $name,
|
||
':directory_id' => $directoryId,
|
||
':loan_institution' => $loanInstitution,
|
||
':borrower' => $borrower,
|
||
':credit_limit' => $creditLimitVal,
|
||
':remaining_limit' => $remainingAuto,
|
||
':start_date' => $startDateVal,
|
||
':end_date' => $endDateFinal,
|
||
':annual_rate' => ($_POST['annual_rate'] ?? '') === '' ? null : (float)$_POST['annual_rate'],
|
||
':guarantee_type' => trim((string)($_POST['guarantee_type'] ?? '')),
|
||
':guarantor' => $guarantorEarly,
|
||
':archive_kind' => $archiveKind,
|
||
':updated_at' => now(),
|
||
];
|
||
if ($id > 0) {
|
||
$sql = 'UPDATE archives SET name=:name,directory_id=:directory_id,archive_kind=:archive_kind,loan_institution=:loan_institution,borrower=:borrower,credit_limit=:credit_limit,remaining_limit=:remaining_limit,start_date=:start_date,end_date=:end_date,annual_rate=:annual_rate,guarantee_type=:guarantee_type,guarantor=:guarantor,updated_at=:updated_at WHERE id=:id';
|
||
$payload[':id'] = $id;
|
||
$pdo->prepare($sql)->execute($payload);
|
||
$archiveId = $id;
|
||
flash('ok', '档案已更新(' . date('H:i:s') . ')');
|
||
} else {
|
||
$sql = 'INSERT INTO archives(name,directory_id,archive_kind,loan_institution,borrower,credit_limit,remaining_limit,start_date,end_date,annual_rate,guarantee_type,guarantor,created_at,updated_at) VALUES(:name,:directory_id,:archive_kind,:loan_institution,:borrower,:credit_limit,:remaining_limit,:start_date,:end_date,:annual_rate,:guarantee_type,:guarantor,:created_at,:updated_at)';
|
||
$payload[':created_at'] = now();
|
||
$pdo->prepare($sql)->execute($payload);
|
||
$archiveId = (int)$pdo->lastInsertId();
|
||
flash('ok', '档案已创建(' . date('H:i:s') . ')');
|
||
}
|
||
|
||
if ($planEnabled) {
|
||
$allPlans[(string)$archiveId] = ['enabled' => true, 'plans' => $plans];
|
||
} else {
|
||
$allPlans[(string)$archiveId] = ['enabled' => false, 'plans' => []];
|
||
}
|
||
saveRepaymentPlans($allPlans);
|
||
|
||
$financeAll = loadFinanceConfigs();
|
||
$interestMode = (string)($_POST['interest_mode'] ?? 'monthly');
|
||
if (!in_array($interestMode, ['monthly', 'quarterly', 'bullet'], true)) {
|
||
$interestMode = 'monthly';
|
||
}
|
||
$settlementRule = $interestMode === 'quarterly' ? 'quarterly_day' : ($interestMode === 'bullet' ? 'maturity_once' : 'monthly_day');
|
||
$settlementDay = $interestMode === 'bullet' ? 0 : max(1, min(28, (int)($_POST['settlement_day'] ?? 21)));
|
||
$todaySave = date('Y-m-d');
|
||
$endDatePosted = $endDateFinal !== null && $endDateFinal !== '' ? (string)$endDateFinal : '';
|
||
$postedEarlyRaw = $_POST['is_early_settled'] ?? null;
|
||
$prevFinance = getArchiveFinanceConfig($financeAll, $archiveId);
|
||
if ($endDatePosted !== '' && $endDatePosted < $todaySave) {
|
||
// 已过到期日:禁止在编辑页首次标「已完成」(须走「待更新」);已是已完成的保留,除非表单显式提交为否
|
||
if (!empty($prevFinance['manual_early_settled'])) {
|
||
$manualEarlySettled = ($postedEarlyRaw === null) ? true : ($postedEarlyRaw === '1');
|
||
} else {
|
||
$manualEarlySettled = false;
|
||
}
|
||
} else {
|
||
$manualEarlySettled = $endDatePosted !== '' && ($postedEarlyRaw === '1');
|
||
}
|
||
$manualOverdue = (($_POST['is_overdue'] ?? '0') === '1');
|
||
if ($manualEarlySettled) {
|
||
$manualOverdue = false;
|
||
}
|
||
$remarkRaw = trim((string)($_POST['archive_remark'] ?? ''));
|
||
if (function_exists('mb_substr')) {
|
||
$remarkRaw = mb_substr($remarkRaw, 0, 4000, 'UTF-8');
|
||
} elseif (strlen($remarkRaw) > 4000) {
|
||
$remarkRaw = substr($remarkRaw, 0, 4000);
|
||
}
|
||
$financeMergeBase = $financeAll[(string)$archiveId] ?? [];
|
||
$financeAll[(string)$archiveId] = array_merge($financeMergeBase, [
|
||
'interest_mode' => $interestMode,
|
||
'settlement_rule' => $settlementRule,
|
||
'settlement_day' => $settlementDay,
|
||
'overdue_float_pct' => max(0, (float)($_POST['overdue_float_pct'] ?? 50)),
|
||
'manual_overdue' => $manualOverdue,
|
||
'manual_early_settled' => $manualEarlySettled,
|
||
'remark' => $remarkRaw,
|
||
'manual_principal_repaid' => $manualPrincipalTotal,
|
||
'manual_interest_paid' => $manualInterestTotal,
|
||
'updated_at' => now(),
|
||
]);
|
||
saveFinanceConfigs($financeAll);
|
||
uploadArchiveFiles($pdo, $archiveId, $_FILES['new_files'] ?? []);
|
||
header('Location: index.php?action=archive_form&id=' . $archiveId);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'reminder_action') {
|
||
requireAdmin();
|
||
$op = (string)($_POST['op'] ?? $_GET['op'] ?? '');
|
||
$archiveId = (int)($_POST['archive_id'] ?? $_GET['archive_id'] ?? 0);
|
||
$planId = trim((string)($_POST['plan_id'] ?? $_GET['plan_id'] ?? ''));
|
||
$redirectType = (string)($_POST['redirect_type'] ?? $_GET['redirect_type'] ?? 'all');
|
||
if (!in_array($redirectType, ['all', 'due', 'plan', 'overdue'], true)) {
|
||
$redirectType = 'all';
|
||
}
|
||
$back = 'index.php?action=reminders&type=' . $redirectType;
|
||
|
||
if ($archiveId <= 0) {
|
||
flash('error', '参数错误');
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
if ($op === 'early_settle') {
|
||
$financeAll = loadFinanceConfigs();
|
||
$cfg = getArchiveFinanceConfig($financeAll, $archiveId);
|
||
$cfg['manual_early_settled'] = true;
|
||
$cfg['manual_overdue'] = false;
|
||
$cfg['updated_at'] = now();
|
||
$financeAll[(string)$archiveId] = $cfg;
|
||
saveFinanceConfigs($financeAll);
|
||
flash('ok', '已设为提前还款,状态改为已完成');
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
if ($op === 'plan_delay') {
|
||
$newDue = trim((string)($_POST['new_due_date'] ?? $_GET['new_due_date'] ?? ''));
|
||
if ($planId === '' || $newDue === '') {
|
||
flash('error', '延期参数不完整');
|
||
redirectBackOr($back);
|
||
}
|
||
$allPlans = loadRepaymentPlans();
|
||
$cfg = getArchiveRepaymentPlan($allPlans, $archiveId);
|
||
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
|
||
flash('error', '分期计划不存在');
|
||
redirectBackOr($back);
|
||
}
|
||
$ok = false;
|
||
foreach ($cfg['plans'] as &$p) {
|
||
if ((string)($p['id'] ?? '') !== $planId) {
|
||
continue;
|
||
}
|
||
$p['due_date'] = $newDue;
|
||
$p['status'] = 'delayed';
|
||
$p['paid_at'] = null;
|
||
$p['updated_at'] = now();
|
||
$ok = true;
|
||
break;
|
||
}
|
||
unset($p);
|
||
if ($ok) {
|
||
$allPlans[(string)$archiveId] = $cfg;
|
||
saveRepaymentPlans($allPlans);
|
||
flash('ok', '还款计划已延期');
|
||
} else {
|
||
flash('error', '未找到对应还款计划');
|
||
}
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
if ($op === 'overdue_paid') {
|
||
$allPlans = loadRepaymentPlans();
|
||
$cfg = getArchiveRepaymentPlan($allPlans, $archiveId);
|
||
$handled = false;
|
||
if ($planId !== '' && !empty($cfg['enabled']) && !empty($cfg['plans']) && is_array($cfg['plans'])) {
|
||
foreach ($cfg['plans'] as &$p) {
|
||
if ((string)($p['id'] ?? '') !== $planId) {
|
||
continue;
|
||
}
|
||
$p['status'] = 'paid';
|
||
$p['paid_at'] = date('Y-m-d');
|
||
$p['updated_at'] = now();
|
||
$handled = true;
|
||
break;
|
||
}
|
||
unset($p);
|
||
if ($handled) {
|
||
$allPlans[(string)$archiveId] = $cfg;
|
||
saveRepaymentPlans($allPlans);
|
||
recalcRemainingLimitByPlans($pdo, $archiveId, (array)$cfg['plans']);
|
||
flash('ok', '逾期分期已标记为已归还');
|
||
}
|
||
} else {
|
||
$financeAll = loadFinanceConfigs();
|
||
$f = getArchiveFinanceConfig($financeAll, $archiveId);
|
||
$f['manual_early_settled'] = true;
|
||
$f['manual_overdue'] = false;
|
||
$f['updated_at'] = now();
|
||
$financeAll[(string)$archiveId] = $f;
|
||
saveFinanceConfigs($financeAll);
|
||
flash('ok', '整笔逾期已归还,状态改为已完成');
|
||
$handled = true;
|
||
}
|
||
if (!$handled) {
|
||
flash('error', '未找到可处理的逾期项');
|
||
}
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
flash('error', '不支持的操作');
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
if ($action === 'pending_update_action') {
|
||
requireAdmin();
|
||
$op = (string)($_POST['op'] ?? '');
|
||
$archiveId = (int)($_POST['archive_id'] ?? 0);
|
||
$planId = trim((string)($_POST['plan_id'] ?? ''));
|
||
$todayPu = date('Y-m-d');
|
||
$dirIdPost = (int)($_POST['dir_id'] ?? 0);
|
||
$back = 'index.php?action=pending_updates' . ($dirIdPost > 0 ? ('&dir_id=' . $dirIdPost) : '');
|
||
if ($archiveId <= 0) {
|
||
flash('error', '参数错误');
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
if ($op === 'pending_loan_repaid' || $op === 'pending_loan_overdue') {
|
||
$st = $pdo->prepare('SELECT end_date FROM archives WHERE id=:id');
|
||
$st->execute([':id' => $archiveId]);
|
||
$endRow = $st->fetchColumn();
|
||
$endStr = $endRow !== false && $endRow !== null ? (string)$endRow : '';
|
||
if ($endStr === '' || $endStr >= $todayPu) {
|
||
flash('error', '该贷款尚未到期待更新处理');
|
||
redirectBackOr($back);
|
||
}
|
||
$financeAll = loadFinanceConfigs();
|
||
$f = getArchiveFinanceConfig($financeAll, $archiveId);
|
||
if (archiveStatusWithConfig($endStr, $f) !== '待更新') {
|
||
flash('error', '该项已处理或状态已变更');
|
||
redirectBackOr($back);
|
||
}
|
||
if ($op === 'pending_loan_repaid') {
|
||
$f['manual_early_settled'] = true;
|
||
$f['manual_overdue'] = false;
|
||
$f['updated_at'] = now();
|
||
$financeAll[(string)$archiveId] = $f;
|
||
saveFinanceConfigs($financeAll);
|
||
flash('ok', '已标记为已还款,状态改为已完成');
|
||
} else {
|
||
$f['manual_overdue'] = true;
|
||
$f['manual_early_settled'] = false;
|
||
$f['updated_at'] = now();
|
||
$financeAll[(string)$archiveId] = $f;
|
||
saveFinanceConfigs($financeAll);
|
||
flash('ok', '已标记为已逾期,状态改为逾期中');
|
||
}
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
if ($op === 'pending_plan_repaid' || $op === 'pending_plan_overdue') {
|
||
if ($planId === '') {
|
||
flash('error', '参数错误');
|
||
redirectBackOr($back);
|
||
}
|
||
$allPlans = loadRepaymentPlans();
|
||
$cfg = getArchiveRepaymentPlan($allPlans, $archiveId);
|
||
$handled = false;
|
||
if (!empty($cfg['enabled']) && !empty($cfg['plans']) && is_array($cfg['plans'])) {
|
||
foreach ($cfg['plans'] as &$p) {
|
||
if ((string)($p['id'] ?? '') !== $planId) {
|
||
continue;
|
||
}
|
||
$pst = (string)($p['status'] ?? 'pending');
|
||
$d = (string)($p['due_date'] ?? '');
|
||
if ($d === '' || $d >= $todayPu || !in_array($pst, ['pending', 'delayed'], true)) {
|
||
break;
|
||
}
|
||
if ($op === 'pending_plan_repaid') {
|
||
$p['status'] = 'paid';
|
||
$p['paid_at'] = date('Y-m-d');
|
||
} else {
|
||
$p['status'] = 'overdue';
|
||
$p['paid_at'] = null;
|
||
}
|
||
$p['updated_at'] = now();
|
||
$handled = true;
|
||
break;
|
||
}
|
||
unset($p);
|
||
}
|
||
if ($handled) {
|
||
$allPlans[(string)$archiveId] = $cfg;
|
||
saveRepaymentPlans($allPlans);
|
||
if ($op === 'pending_plan_repaid') {
|
||
recalcRemainingLimitByPlans($pdo, $archiveId, (array)$cfg['plans']);
|
||
}
|
||
flash('ok', $op === 'pending_plan_repaid' ? '已标记为已还款(按期还款)' : '已标记为已逾期(逾期中)');
|
||
} else {
|
||
flash('error', '未找到可处理的还款计划或状态已变更');
|
||
}
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
flash('error', '不支持的操作');
|
||
redirectBackOr($back);
|
||
}
|
||
|
||
if ($action === 'download_import_template') {
|
||
requireAdmin();
|
||
$manualTemplatePath = __DIR__ . '/../data/import_template.xlsx';
|
||
if (is_file($manualTemplatePath)) {
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode('档案导入模板.xlsx') . '"');
|
||
readfile($manualTemplatePath);
|
||
exit;
|
||
}
|
||
$headers = ['一级目录', '二级目录', '三级目录', '档案名称', '贷款机构', '借款主体', '授信额度', '起止日', '到期日', '年利率', '担保类型', '担保人', '计息方式', '结息日', '是否有分期还款计划'];
|
||
$sample = ['银行贷款', '2026', 'XX银行', 'A公司流动资金贷款合同', 'XX银行', 'A公司', '5000000', '2026-01-15', '2027-01-14', '5.5', '抵押', 'B公司', '按月计息', '21', '否'];
|
||
if (class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
|
||
$filename = '档案导入模板.xlsx';
|
||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$sheet->setCellValue(
|
||
'A1',
|
||
'说明:第1行可写模板使用提示(导入时忽略);第2行为列标题勿改;从第3行起填数据。'
|
||
. '一级目录填「银行贷款」或「对外担保」;系统据此识别档案类型。对外担保导入时「贷款机构」「年利率」可留空;「档案名称」可留空,将按担保人+对外担保+授信万元自动生成。'
|
||
. '银行贷款必填至担保人(含档案名称列);选填:计息方式、结息日、是否有分期还款计划。'
|
||
);
|
||
$sheet->fromArray($headers, null, 'A2');
|
||
$sheet->fromArray([$sample], null, 'A3');
|
||
foreach (range('A', 'O') as $col) {
|
||
$sheet->getColumnDimension($col)->setWidth(20);
|
||
}
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
|
||
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
|
||
$writer->save('php://output');
|
||
exit;
|
||
}
|
||
flash('error', '未找到可用xlsx模板。请安装PhpSpreadsheet或将模板放到 data/import_template.xlsx');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'import_archives') {
|
||
requireAdmin();
|
||
if (empty($_FILES['import_file']) || (int)$_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
|
||
flash('error', '请选择有效导入文件');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$name = (string)$_FILES['import_file']['name'];
|
||
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
||
$rows = [];
|
||
if ($ext === 'csv') {
|
||
$fp = fopen((string)$_FILES['import_file']['tmp_name'], 'rb');
|
||
if ($fp) {
|
||
fgetcsv($fp);
|
||
$header = fgetcsv($fp);
|
||
while (($line = fgetcsv($fp)) !== false) {
|
||
$rows[] = $line;
|
||
}
|
||
fclose($fp);
|
||
}
|
||
} elseif (in_array($ext, ['xlsx', 'xls'], true) && class_exists('\PhpOffice\PhpSpreadsheet\IOFactory')) {
|
||
$sheet = \PhpOffice\PhpSpreadsheet\IOFactory::load((string)$_FILES['import_file']['tmp_name'])->getActiveSheet();
|
||
$arr = $sheet->toArray();
|
||
array_shift($arr);
|
||
$header = array_shift($arr);
|
||
$rows = $arr;
|
||
} else {
|
||
flash('error', '仅支持CSV,或安装PhpSpreadsheet后支持Excel');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$created = 0;
|
||
$skipped = 0;
|
||
$financeAll = loadFinanceConfigs();
|
||
$planAll = loadRepaymentPlans();
|
||
$seen = [];
|
||
foreach ($rows as $r) {
|
||
$lv1 = (string)($r[0] ?? '');
|
||
$lv2 = (string)($r[1] ?? '');
|
||
$lv3 = (string)($r[2] ?? '');
|
||
$archiveName = trim((string)($r[3] ?? ''));
|
||
$loanInstitution = trim((string)($r[4] ?? ''));
|
||
$borrower = trim((string)($r[5] ?? ''));
|
||
$creditRaw = trim((string)($r[6] ?? ''));
|
||
$startDate = trim((string)($r[7] ?? ''));
|
||
$endDate = trim((string)($r[8] ?? ''));
|
||
$annualRateRaw = trim((string)($r[9] ?? ''));
|
||
$guaranteeType = trim((string)($r[10] ?? ''));
|
||
$guarantor = trim((string)($r[11] ?? ''));
|
||
$importKind = trim($lv1) === '对外担保' ? ARCHIVE_KIND_EXTERNAL_GUARANTEE : ARCHIVE_KIND_BANK_LOAN;
|
||
if ($importKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
|
||
if ($lv1 === '' || $lv2 === '' || $lv3 === '' || $borrower === '' || $creditRaw === '' || $startDate === '' || $endDate === '' || $guaranteeType === '' || $guarantor === '') {
|
||
continue;
|
||
}
|
||
$annualRate = $annualRateRaw === '' ? null : (float) $annualRateRaw;
|
||
} else {
|
||
if ($lv1 === '' || $lv2 === '' || $lv3 === '' || $archiveName === '' || $loanInstitution === '' || $borrower === '' || $creditRaw === '' || $startDate === '' || $endDate === '' || $annualRateRaw === '' || $guaranteeType === '' || $guarantor === '') {
|
||
continue;
|
||
}
|
||
$annualRate = (float) $annualRateRaw;
|
||
}
|
||
// 无论目录是否预先存在,都会自动按三级路径创建并返回三级目录ID。
|
||
$dirId = ensureDirectoryPath($pdo, $lv1, $lv2, $lv3);
|
||
$credit = (float) $creditRaw;
|
||
$remain = $credit;
|
||
if ($importKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE) {
|
||
$archiveName = autoExternalGuaranteeArchiveName($guarantor, $credit);
|
||
if ($archiveName === '') {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$fingerprint = implode('|', [
|
||
$importKind,
|
||
(string) $dirId,
|
||
mb_strtolower($archiveName),
|
||
mb_strtolower($loanInstitution),
|
||
mb_strtolower($borrower),
|
||
(string) $credit,
|
||
$startDate,
|
||
$endDate,
|
||
(string) ($annualRate ?? ''),
|
||
mb_strtolower($guaranteeType),
|
||
mb_strtolower($guarantor),
|
||
]);
|
||
if (isset($seen[$fingerprint])) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
$seen[$fingerprint] = true;
|
||
|
||
$dupStmt = $pdo->prepare('SELECT COUNT(*) FROM archives WHERE directory_id=:d AND archive_kind=:ak AND name=:n AND COALESCE(loan_institution,\'\')=:li AND borrower=:b AND credit_limit=:cl AND start_date=:sd AND end_date=:ed AND COALESCE(annual_rate,-999999.99)=COALESCE(:ar,-999999.99) AND guarantee_type=:gt AND guarantor=:g');
|
||
$dupStmt->execute([
|
||
':d' => $dirId,
|
||
':ak' => $importKind,
|
||
':n' => $archiveName,
|
||
':li' => $loanInstitution,
|
||
':b' => $borrower,
|
||
':cl' => $credit,
|
||
':sd' => $startDate,
|
||
':ed' => $endDate,
|
||
':ar' => $annualRate,
|
||
':gt' => $guaranteeType,
|
||
':g' => $guarantor,
|
||
]);
|
||
if ((int)$dupStmt->fetchColumn() > 0) {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$pdo->prepare('INSERT INTO archives(name,directory_id,archive_kind,loan_institution,borrower,credit_limit,remaining_limit,start_date,end_date,annual_rate,guarantee_type,guarantor,created_at,updated_at) VALUES(:name,:directory_id,:archive_kind,:loan_institution,:borrower,:credit_limit,:remaining_limit,:start_date,:end_date,:annual_rate,:guarantee_type,:guarantor,:created_at,:updated_at)')
|
||
->execute([
|
||
':name' => $archiveName,
|
||
':directory_id' => $dirId,
|
||
':archive_kind' => $importKind,
|
||
':loan_institution' => $loanInstitution,
|
||
':borrower' => $borrower,
|
||
':credit_limit' => $credit,
|
||
':remaining_limit' => $remain,
|
||
':start_date' => $startDate ?: null,
|
||
':end_date' => $endDate ?: null,
|
||
':annual_rate' => $annualRate,
|
||
':guarantee_type' => $guaranteeType,
|
||
':guarantor' => $guarantor,
|
||
':created_at' => now(),
|
||
':updated_at' => now(),
|
||
]);
|
||
$archiveId = (int)$pdo->lastInsertId();
|
||
$modeText = trim((string)($r[12] ?? ''));
|
||
$mode = $modeText === '按季付息' ? 'quarterly' : ($modeText === '到期一次性付清' ? 'bullet' : 'monthly');
|
||
$settlementRule = $mode === 'quarterly' ? 'quarterly_day' : ($mode === 'bullet' ? 'maturity_once' : 'monthly_day');
|
||
$settlementDay = $mode === 'bullet' ? 0 : max(1, min(28, (int)trim((string)($r[13] ?? '21'))));
|
||
$financeAll[(string)$archiveId] = [
|
||
'interest_mode' => $mode,
|
||
'settlement_rule' => $settlementRule,
|
||
'settlement_day' => $settlementDay,
|
||
'overdue_float_pct' => 50,
|
||
'manual_overdue' => false,
|
||
'manual_early_settled' => false,
|
||
'remark' => '',
|
||
'updated_at' => now(),
|
||
];
|
||
$planEnabledText = trim((string)($r[14] ?? '否'));
|
||
$planAll[(string)$archiveId] = ['enabled' => in_array($planEnabledText, ['是', 'yes', 'YES', '1'], true), 'plans' => []];
|
||
$created++;
|
||
}
|
||
saveFinanceConfigs($financeAll);
|
||
saveRepaymentPlans($planAll);
|
||
flash('ok', '导入完成:新增 ' . $created . ' 条,重复忽略 ' . $skipped . ' 条');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'request_delete_archive') {
|
||
requireAdmin();
|
||
$archiveId = (int)($_GET['id'] ?? 0);
|
||
if ($archiveId <= 0) {
|
||
flash('error', '参数错误');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$approvals = loadDeleteApprovals();
|
||
foreach ($approvals as $ap) {
|
||
if (($ap['target_type'] ?? '') === 'archive' && (int)($ap['target_id'] ?? 0) === $archiveId && ($ap['status'] ?? '') === 'pending') {
|
||
flash('error', '该档案已有待审核删除申请');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
}
|
||
$approvals[] = [
|
||
'id' => uniqid('del_', true),
|
||
'target_type' => 'archive',
|
||
'target_id' => $archiveId,
|
||
'status' => 'pending',
|
||
'requested_by' => (int)$me['id'],
|
||
'requested_name' => (string)$me['username'],
|
||
'requested_at' => now(),
|
||
'approved_by' => null,
|
||
'approved_at' => null,
|
||
];
|
||
saveDeleteApprovals($approvals);
|
||
flash('ok', '已提交删除申请,需另一位管理员审核');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'request_delete_selected') {
|
||
requireAdmin();
|
||
$idsRaw = $_GET['ids'] ?? '';
|
||
if (is_array($idsRaw)) {
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', $idsRaw))));
|
||
} else {
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', explode(',', (string)$idsRaw)))));
|
||
}
|
||
if (empty($ids)) {
|
||
flash('error', '请先选择要删除的档案');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$approvals = loadDeleteApprovals();
|
||
$pendingSet = [];
|
||
foreach ($approvals as $ap) {
|
||
if (($ap['status'] ?? '') !== 'pending') {
|
||
continue;
|
||
}
|
||
if (($ap['target_type'] ?? '') === 'archive' && !empty($ap['target_id'])) {
|
||
$pendingSet[] = (string)$ap['target_id'];
|
||
}
|
||
if (($ap['target_type'] ?? '') === 'archive_batch' && !empty($ap['target_ids']) && is_array($ap['target_ids'])) {
|
||
foreach ($ap['target_ids'] as $x) {
|
||
$pendingSet[] = (string)(int)$x;
|
||
}
|
||
}
|
||
}
|
||
foreach ($ids as $id) {
|
||
if (in_array((string)$id, $pendingSet, true)) {
|
||
flash('error', '所选档案中存在待审核删除项,请先完成审核');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
}
|
||
$approvals[] = [
|
||
'id' => uniqid('del_', true),
|
||
'target_type' => 'archive_batch',
|
||
'target_ids' => $ids,
|
||
'target_count' => count($ids),
|
||
'status' => 'pending',
|
||
'requested_by' => (int)$me['id'],
|
||
'requested_name' => (string)$me['username'],
|
||
'requested_at' => now(),
|
||
'approved_by' => null,
|
||
'approved_at' => null,
|
||
];
|
||
saveDeleteApprovals($approvals);
|
||
flash('ok', '已提交选中档案删除申请(' . count($ids) . ' 条),需另一位管理员审核');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'delete_approvals') {
|
||
requireAdmin();
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$approvalId = trim((string)($_POST['approval_id'] ?? ''));
|
||
$do = (string)($_POST['do'] ?? '');
|
||
$approvals = loadDeleteApprovals();
|
||
foreach ($approvals as &$ap) {
|
||
if (($ap['id'] ?? '') !== $approvalId || ($ap['status'] ?? '') !== 'pending') {
|
||
continue;
|
||
}
|
||
if ((int)$ap['requested_by'] === (int)$me['id']) {
|
||
flash('error', '不能由申请人本人审核');
|
||
break;
|
||
}
|
||
if ($do === 'approve' && ($ap['target_type'] ?? '') === 'archive') {
|
||
hardDeleteArchive($pdo, (int)$ap['target_id']);
|
||
$ap['status'] = 'approved';
|
||
$ap['approved_by'] = (int)$me['id'];
|
||
$ap['approved_at'] = now();
|
||
flash('ok', '删除已由第二管理员审核并执行');
|
||
break;
|
||
}
|
||
if ($do === 'approve' && ($ap['target_type'] ?? '') === 'archive_batch') {
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', (array)($ap['target_ids'] ?? [])))));
|
||
foreach ($ids as $aid) {
|
||
hardDeleteArchive($pdo, $aid);
|
||
}
|
||
$ap['status'] = 'approved';
|
||
$ap['approved_by'] = (int)$me['id'];
|
||
$ap['approved_at'] = now();
|
||
flash('ok', '批量删除已审核并执行(' . count($ids) . ' 条)');
|
||
break;
|
||
}
|
||
if ($do === 'reject') {
|
||
$ap['status'] = 'rejected';
|
||
$ap['approved_by'] = (int)$me['id'];
|
||
$ap['approved_at'] = now();
|
||
flash('ok', '已驳回删除申请');
|
||
break;
|
||
}
|
||
}
|
||
unset($ap);
|
||
saveDeleteApprovals($approvals);
|
||
redirectBackOr('index.php?action=delete_approvals');
|
||
}
|
||
$approvals = array_reverse(loadDeleteApprovals());
|
||
$filterStatus = (string)($_GET['status'] ?? 'pending');
|
||
$filterTarget = trim((string)($_GET['target_id'] ?? ''));
|
||
$filterRequester = trim((string)($_GET['requester'] ?? ''));
|
||
$approvals = array_values(array_filter($approvals, function ($ap) use ($filterStatus, $filterTarget, $filterRequester) {
|
||
if ($filterStatus !== 'all' && ($ap['status'] ?? '') !== $filterStatus) {
|
||
return false;
|
||
}
|
||
if ($filterTarget !== '' && (string)($ap['target_id'] ?? '') !== $filterTarget) {
|
||
return false;
|
||
}
|
||
if ($filterRequester !== '' && mb_stripos((string)($ap['requested_name'] ?? ''), $filterRequester) === false) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}));
|
||
$usersMap = [];
|
||
$archiveMap = fetchArchiveSimpleMap($pdo);
|
||
foreach ($pdo->query('SELECT id,username FROM users')->fetchAll() as $u) {
|
||
$usersMap[(int)$u['id']] = (string)$u['username'];
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3 class="section-title">删除审核(双管理员)</h3>
|
||
<form method="get" class="toolbar-left" style="margin-bottom:10px">
|
||
<input type="hidden" name="action" value="delete_approvals">
|
||
<select name="status">
|
||
<option value="pending" <?= $filterStatus === 'pending' ? 'selected' : '' ?>>待审核</option>
|
||
<option value="approved" <?= $filterStatus === 'approved' ? 'selected' : '' ?>>已通过</option>
|
||
<option value="rejected" <?= $filterStatus === 'rejected' ? 'selected' : '' ?>>已驳回</option>
|
||
<option value="all" <?= $filterStatus === 'all' ? 'selected' : '' ?>>全部</option>
|
||
</select>
|
||
<input name="target_id" placeholder="目标ID" value="<?= e($filterTarget) ?>">
|
||
<input name="requester" placeholder="申请人" value="<?= e($filterRequester) ?>">
|
||
<button type="submit">筛选</button>
|
||
<a class="clear-link" href="index.php?action=delete_approvals">清空</a>
|
||
</form>
|
||
<table>
|
||
<tr><th>申请时间</th><th>类型</th><th>目标</th><th>申请人</th><th>状态</th><th>审核人</th><th>审核时间</th><th>操作</th></tr>
|
||
<?php foreach ($approvals as $ap): ?>
|
||
<tr>
|
||
<td><?= e((string)$ap['requested_at']) ?></td>
|
||
<td><?= e((string)$ap['target_type']) ?></td>
|
||
<td><?= e(approvalTargetLabel($ap, $archiveMap)) ?></td>
|
||
<td><?= e((string)$ap['requested_name']) ?></td>
|
||
<td><?= e((string)$ap['status']) ?></td>
|
||
<td><?php if (isset($usersMap[(int)($ap['approved_by'] ?? 0)])): ?><?= e($usersMap[(int)$ap['approved_by']]) ?><?php else: ?><span class="muted">-</span><?php endif; ?></td>
|
||
<td><?php if (!empty($ap['approved_at'])): ?><?= e((string)$ap['approved_at']) ?><?php else: ?><span class="muted">-</span><?php endif; ?></td>
|
||
<td>
|
||
<?php if (($ap['status'] ?? '') === 'pending'): ?>
|
||
<form class="inline" method="post">
|
||
<input type="hidden" name="approval_id" value="<?= e((string)$ap['id']) ?>">
|
||
<input type="hidden" name="do" value="approve">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit">审核通过并删除</button>
|
||
</form>
|
||
<form class="inline" method="post">
|
||
<input type="hidden" name="approval_id" value="<?= e((string)$ap['id']) ?>">
|
||
<input type="hidden" name="do" value="reject">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button class="btn-gray" type="submit">驳回</button>
|
||
</form>
|
||
<?php else: ?>
|
||
<span class="muted">-</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($approvals)): ?>
|
||
<tr><td colspan="8" class="muted">暂无符合条件的审核记录</td></tr>
|
||
<?php endif; ?>
|
||
</table>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'delete_archive') {
|
||
requireAdmin();
|
||
flash('error', '请通过双管理员删除流程操作');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'upload_file') {
|
||
requireAdmin();
|
||
$archiveId = (int)($_GET['archive_id'] ?? $_POST['archive_id'] ?? 0);
|
||
if ($archiveId <= 0 || empty($_FILES['files'])) {
|
||
flash('error', '上传参数错误');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
uploadArchiveFiles($pdo, $archiveId, $_FILES['files']);
|
||
flash('ok', '上传完成');
|
||
header('Location: index.php?action=archive_form&id=' . $archiveId);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'delete_file') {
|
||
requireAdmin();
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
$stmt = $pdo->prepare('SELECT * FROM files WHERE id=:id');
|
||
$stmt->execute([':id' => $id]);
|
||
$f = $stmt->fetch();
|
||
if ($f) {
|
||
$p = UPLOAD_DIR . '/' . $f['stored_name'];
|
||
if (is_file($p)) {
|
||
unlink($p);
|
||
}
|
||
$pdo->prepare('DELETE FROM files WHERE id=:id')->execute([':id' => $id]);
|
||
flash('ok', '文件已删除');
|
||
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
|
||
exit;
|
||
}
|
||
flash('error', '文件不存在');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'rename_file') {
|
||
requireAdmin();
|
||
$fileId = (int)($_POST['file_id'] ?? 0);
|
||
$newNameRaw = trim((string)($_POST['new_name'] ?? ''));
|
||
if ($fileId <= 0 || $newNameRaw === '') {
|
||
flash('error', '重命名参数错误');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$stmt = $pdo->prepare('SELECT * FROM files WHERE id=:id');
|
||
$stmt->execute([':id' => $fileId]);
|
||
$f = $stmt->fetch();
|
||
if (!$f) {
|
||
flash('error', '文件不存在');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
|
||
$currentStored = (string)$f['stored_name'];
|
||
$dirPart = dirname($currentStored);
|
||
$dirPart = $dirPart === '.' ? '' : $dirPart;
|
||
$prefix = pathinfo($currentStored, PATHINFO_FILENAME);
|
||
$dotPos = strpos($prefix, '_');
|
||
$uniqPrefix = $dotPos === false ? uniqid('f_', true) : substr($prefix, 0, $dotPos);
|
||
$ext = strtolower((string)$f['file_ext']);
|
||
|
||
$newOriginal = $newNameRaw;
|
||
if (strtolower((string)pathinfo($newOriginal, PATHINFO_EXTENSION)) !== $ext) {
|
||
$newOriginal .= '.' . $ext;
|
||
}
|
||
$normalized = normalizeFileName($newOriginal);
|
||
$newBase = $uniqPrefix . '_' . $normalized;
|
||
$newStored = ($dirPart !== '' ? $dirPart . '/' : '') . $newBase;
|
||
|
||
$oldPath = UPLOAD_DIR . '/' . $currentStored;
|
||
$newPath = UPLOAD_DIR . '/' . $newStored;
|
||
if (!is_file($oldPath)) {
|
||
flash('error', '原文件不存在');
|
||
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
|
||
exit;
|
||
}
|
||
if (is_file($newPath)) {
|
||
$newStored = ($dirPart !== '' ? $dirPart . '/' : '') . uniqid('f_', true) . '_' . $normalized;
|
||
$newPath = UPLOAD_DIR . '/' . $newStored;
|
||
}
|
||
if (!rename($oldPath, $newPath)) {
|
||
flash('error', '文件重命名失败');
|
||
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
|
||
exit;
|
||
}
|
||
|
||
$pdo->prepare('UPDATE files SET original_name=:o, stored_name=:s WHERE id=:id')
|
||
->execute([':o' => $newOriginal, ':s' => $newStored, ':id' => $fileId]);
|
||
flash('ok', '文件已重命名');
|
||
header('Location: index.php?action=archive_form&id=' . (int)$f['archive_id']);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'download_file') {
|
||
requireLogin();
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
$stmt = $pdo->prepare('SELECT * FROM files WHERE id=:id');
|
||
$stmt->execute([':id' => $id]);
|
||
$f = $stmt->fetch();
|
||
if (!$f) {
|
||
http_response_code(404);
|
||
exit('Not found');
|
||
}
|
||
$p = UPLOAD_DIR . '/' . $f['stored_name'];
|
||
if (!is_file($p)) {
|
||
http_response_code(404);
|
||
exit('File missing');
|
||
}
|
||
header('Content-Type: application/octet-stream');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode($f['original_name']) . '"');
|
||
readfile($p);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'preview_file') {
|
||
requireLogin();
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
$stmt = $pdo->prepare('SELECT * FROM files WHERE id=:id');
|
||
$stmt->execute([':id' => $id]);
|
||
$f = $stmt->fetch();
|
||
if (!$f || !canPreview($f['file_ext'])) {
|
||
http_response_code(403);
|
||
exit('不支持预览');
|
||
}
|
||
$p = UPLOAD_DIR . '/' . $f['stored_name'];
|
||
if (!is_file($p)) {
|
||
http_response_code(404);
|
||
exit('文件不存在');
|
||
}
|
||
if ($f['file_ext'] === 'pdf') {
|
||
header('Content-Type: application/pdf');
|
||
} else {
|
||
header('Content-Type: image/' . ($f['file_ext'] === 'jpg' ? 'jpeg' : $f['file_ext']));
|
||
}
|
||
readfile($p);
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'archive_detail') {
|
||
requireLogin();
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
$stmt = $pdo->prepare('SELECT a.*, d.name AS dir_name FROM archives a JOIN directories d ON a.directory_id=d.id WHERE a.id=:id');
|
||
$stmt->execute([':id' => $id]);
|
||
$a = $stmt->fetch();
|
||
if (!$a) {
|
||
flash('error', '档案不存在');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
$planAll = loadRepaymentPlans();
|
||
$planCfg = getArchiveRepaymentPlan($planAll, $id);
|
||
$repaymentPlans = $planCfg['plans'] ?? [];
|
||
$financeAll = loadFinanceConfigs();
|
||
$financeCfg = getArchiveFinanceConfig($financeAll, $id);
|
||
$status = archiveStatusWithConfig($a['end_date'], $financeCfg);
|
||
$detailKind = normalizeArchiveKind($a['archive_kind'] ?? '');
|
||
$interestPayable = calcCurrentInterest($a, $financeCfg, $repaymentPlans);
|
||
$settlementInterestPayable = $detailKind === ARCHIVE_KIND_BANK_LOAN
|
||
? calcSettlementPeriodInterest($a, $financeCfg, $repaymentPlans, date('Y-m-d'))
|
||
: 0.0;
|
||
usort($repaymentPlans, function ($x, $y) {
|
||
return strcmp((string)($x['due_date'] ?? ''), (string)($y['due_date'] ?? ''));
|
||
});
|
||
$files = $pdo->prepare('SELECT * FROM files WHERE archive_id=:a ORDER BY id DESC');
|
||
$files->execute([':a' => $id]);
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3><?= e($a['name']) ?> <span class="muted">(<?= e($a['dir_name']) ?>)</span></h3>
|
||
<p>状态:<span class="badge <?= $status === '履行中' ? 'b1' : ($status === '已完成' ? 'b2' : ($status === '逾期中' ? 'b4' : 'b3')) ?>"><?= e($status) ?></span>
|
||
<span class="muted" style="margin-left:10px">类型:<?= e(archiveKindLabel($detailKind)) ?></span></p>
|
||
<table>
|
||
<tr><th><?= $detailKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? '债权人' : '贷款机构' ?></th><td><?= e((string)$a['loan_institution']) ?></td><th>借款主体</th><td><?= e((string)$a['borrower']) ?></td></tr>
|
||
<tr><th>授信额度</th><td><?= e(amount($a['credit_limit'] !== null ? (float)$a['credit_limit'] : null)) ?></td><th>剩余额度</th><td><?= e(amount($a['remaining_limit'] !== null ? (float)$a['remaining_limit'] : null)) ?></td></tr>
|
||
<tr><th>起止日</th><td><?= e((string)$a['start_date']) ?></td><th>到期日</th><td><?= e((string)$a['end_date']) ?></td></tr>
|
||
<tr><th>年利率</th><td><?= $detailKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? '<span class="muted">—</span>' : e($a['annual_rate'] === null ? '' : ((string)$a['annual_rate'] . '%')) ?></td><th>担保类型</th><td><?= e((string)$a['guarantee_type']) ?></td></tr>
|
||
<tr><th>担保人</th><td colspan="3"><?= e((string)$a['guarantor']) ?></td></tr>
|
||
<tr>
|
||
<th>结息方式</th>
|
||
<td><?= e($financeCfg['interest_mode'] === 'quarterly' ? '按季计息' : ($financeCfg['interest_mode'] === 'bullet' ? '到期一次性付清' : '按月计息')) ?></td>
|
||
<th>结息日</th>
|
||
<td>
|
||
<?php
|
||
$rule = (string)$financeCfg['settlement_rule'];
|
||
if ($rule === 'quarterly_day') {
|
||
echo '每季' . e((string)$financeCfg['settlement_day']) . '日';
|
||
} elseif ($rule === 'maturity_once') {
|
||
echo '到期一次性付息';
|
||
} else {
|
||
echo '每月' . e((string)$financeCfg['settlement_day']) . '日';
|
||
}
|
||
?>
|
||
</td>
|
||
</tr>
|
||
<tr><th>逾期利率上浮</th><td><?= e(amount((float)$financeCfg['overdue_float_pct'])) ?>%</td><th>当前应付利息</th><td><?= e(amount($interestPayable)) ?> 元</td></tr>
|
||
<?php if ($detailKind === ARCHIVE_KIND_BANK_LOAN): ?>
|
||
<tr><th>结息日应付利息</th><td colspan="3"><?= e(amount($settlementInterestPayable)) ?> 元 <span class="muted small">(本结息周期整段)</span></td></tr>
|
||
<?php endif; ?>
|
||
<?php $detailRemark = trim((string)($financeCfg['remark'] ?? '')); ?>
|
||
<?php if ($detailRemark !== ''): ?>
|
||
<tr><th>备注</th><td colspan="3" style="white-space:pre-wrap"><?= e($detailRemark) ?></td></tr>
|
||
<?php endif; ?>
|
||
</table>
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<h3>分期还款计划</h3>
|
||
<?php if (!($planCfg['enabled'] ?? false)): ?>
|
||
<p class="muted">该档案未启用分期还款计划</p>
|
||
<?php else: ?>
|
||
<table>
|
||
<tr><th>还款日期</th><th>还款金额</th><th>状态</th></tr>
|
||
<?php foreach ($repaymentPlans as $p): ?>
|
||
<tr>
|
||
<td><?= e((string)$p['due_date']) ?></td>
|
||
<td><?= e(amount((float)($p['amount'] ?? 0))) ?></td>
|
||
<td>
|
||
<?php
|
||
$st = (string)($p['status'] ?? 'pending');
|
||
if ($st === 'paid') {
|
||
echo '<span class="badge b1">已还款</span>';
|
||
} elseif ($st === 'delayed') {
|
||
echo '<span class="badge b2">已延期</span>';
|
||
} elseif ($st === 'overdue') {
|
||
echo '<span class="badge b4">已逾期</span>';
|
||
} else {
|
||
echo '<span class="badge b3">待处理</span>';
|
||
}
|
||
?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</table>
|
||
<?php if (isAdmin()): ?><p class="small muted">还款计划状态和延期调整请在“编辑档案”页面操作。</p><?php endif; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<h3>文件列表</h3>
|
||
<table>
|
||
<tr><th>文件名</th><th>大小</th><th>操作</th></tr>
|
||
<?php foreach ($files as $f): ?>
|
||
<tr>
|
||
<td><?= e($f['original_name']) ?></td>
|
||
<td><?= number_format(((int)$f['file_size']) / 1024, 1) ?> KB</td>
|
||
<td>
|
||
<div class="icon-actions">
|
||
<?php if (canPreview($f['file_ext'])): ?><a class="icon-btn" target="_blank" href="index.php?action=preview_file&id=<?= (int)$f['id'] ?>">👁 预览</a><?php endif; ?>
|
||
<a class="icon-btn" href="index.php?action=download_file&id=<?= (int)$f['id'] ?>">⬇ 下载</a>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</table>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'archive_form') {
|
||
requireAdmin();
|
||
$id = (int)($_GET['id'] ?? 0);
|
||
$editing = null;
|
||
if ($id > 0) {
|
||
$st = $pdo->prepare('SELECT * FROM archives WHERE id=:id');
|
||
$st->execute([':id' => $id]);
|
||
$editing = $st->fetch();
|
||
if (!$editing) {
|
||
flash('error', '档案不存在');
|
||
header('Location: index.php');
|
||
exit;
|
||
}
|
||
}
|
||
$dirs = fetchDirectories($pdo);
|
||
$loanInstitutions = loadLoanInstitutions();
|
||
$dirId = (int)($_GET['dir_id'] ?? ($editing['directory_id'] ?? 0));
|
||
$dirPath = dirPathMap($dirs);
|
||
$planAll = loadRepaymentPlans();
|
||
$planCfg = $editing ? getArchiveRepaymentPlan($planAll, (int)$editing['id']) : ['enabled' => false, 'plans' => []];
|
||
$financeAll = loadFinanceConfigs();
|
||
$financeCfg = $editing ? getArchiveFinanceConfig($financeAll, (int)$editing['id']) : [
|
||
'interest_mode' => '',
|
||
'settlement_rule' => 'monthly_day',
|
||
'settlement_day' => 21,
|
||
'overdue_float_pct' => 50,
|
||
'manual_overdue' => false,
|
||
'manual_early_settled' => false,
|
||
'remark' => '',
|
||
'manual_principal_repaid' => 0.0,
|
||
'manual_interest_paid' => 0.0,
|
||
];
|
||
$planRows = $planCfg['plans'] ?? [];
|
||
if (empty($planRows)) {
|
||
for ($i = 0; $i < 6; $i++) {
|
||
$planRows[] = ['id' => '', 'due_date' => '', 'amount' => ''];
|
||
}
|
||
}
|
||
$editArchiveId = (int)($editing['id'] ?? 0);
|
||
$editFiles = [];
|
||
if ($editArchiveId > 0) {
|
||
$fs = $pdo->prepare('SELECT * FROM files WHERE archive_id=:a ORDER BY id DESC');
|
||
$fs->execute([':a' => $editArchiveId]);
|
||
$editFiles = $fs->fetchAll();
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3 class="section-title"><?= $editing ? '编辑档案' : '新建档案' ?></h3>
|
||
<?php $formKind = $editing ? normalizeArchiveKind($editing['archive_kind'] ?? '') : ARCHIVE_KIND_BANK_LOAN; ?>
|
||
<form method="post" action="index.php?action=save_archive" class="archive-form-grid" enctype="multipart/form-data">
|
||
<input type="hidden" name="id" value="<?= (int)($editing['id'] ?? 0) ?>">
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="archive_kind">档案类型</label>
|
||
<select name="archive_kind" id="archive_kind">
|
||
<option value="<?= e(ARCHIVE_KIND_BANK_LOAN) ?>" <?= $formKind === ARCHIVE_KIND_BANK_LOAN ? 'selected' : '' ?>>银行贷款</option>
|
||
<option value="<?= e(ARCHIVE_KIND_EXTERNAL_GUARANTEE) ?>" <?= $formKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? 'selected' : '' ?>>对外担保</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label class="muted" style="font-size:12px">自动目录说明</label>
|
||
<p class="small muted" id="archive_auto_dir_hint" style="margin:0;line-height:1.5"></p>
|
||
</div>
|
||
</div>
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="archive_name_auto">档案名称</label>
|
||
<input id="archive_name_auto" placeholder="根据档案类型自动生成" readonly value="<?= e((string)($editing['name'] ?? '')) ?>">
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="archive_directory_id">三级目录</label>
|
||
<select name="directory_id" id="archive_directory_id">
|
||
<option value="">自动(按档案类型与表单字段创建三级目录)</option>
|
||
<?php foreach ($dirs as $d): if ((int)$d['level'] !== 3) continue; ?>
|
||
<option value="<?= (int)$d['id'] ?>" <?= ((int)($editing['directory_id'] ?? $dirId) === (int)$d['id']) ? 'selected' : '' ?>>
|
||
<?= e($dirPath[(int)$d['id']] ?? $d['name']) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="loan_institution_select" id="loan_institution_label">贷款机构</label>
|
||
<select name="loan_institution" id="loan_institution_select">
|
||
<option value="">请选择贷款机构</option>
|
||
<?php foreach ($loanInstitutions as $li): ?>
|
||
<option value="<?= e($li) ?>" <?= ((string)($editing['loan_institution'] ?? '') === $li) ? 'selected' : '' ?>><?= e($li) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<input type="hidden" name="loan_institutions_json" id="loan_institutions_json" value="">
|
||
<p class="small muted" style="margin:0">
|
||
<a href="javascript:void(0)" onclick="addInstitutionOption()">+ 添加机构</a>
|
||
<span class="muted"> | </span>
|
||
<a href="javascript:void(0)" onclick="removeInstitutionOption()">- 删除当前机构</a>
|
||
</p>
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="borrower_input">借款主体</label>
|
||
<input name="borrower" id="borrower_input" placeholder="借款主体" value="<?= e((string)($editing['borrower'] ?? '')) ?>">
|
||
</div>
|
||
</div>
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="credit_limit_input">授信额度</label>
|
||
<input type="number" step="0.01" name="credit_limit" id="credit_limit_input" placeholder="授信额度" value="<?= e((string)($editing['credit_limit'] ?? '')) ?>">
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="remaining_limit_display">剩余额度</label>
|
||
<input type="text" id="remaining_limit_display" value="<?= e(amount($editing && $editing['remaining_limit'] !== null ? (float)$editing['remaining_limit'] : null)) ?>" placeholder="剩余额度自动计算" readonly>
|
||
</div>
|
||
</div>
|
||
<div class="form-row-2">
|
||
<div class="date-labeled">
|
||
<span class="date-fixed-label">起始日</span>
|
||
<input type="date" name="start_date" id="archive_start_date" value="<?= e((string)($editing['start_date'] ?? '')) ?>">
|
||
</div>
|
||
<div class="date-labeled">
|
||
<span class="date-fixed-label">到期日</span>
|
||
<input type="date" name="end_date" id="loan_end_date" value="<?= e((string)($editing['end_date'] ?? '')) ?>">
|
||
</div>
|
||
</div>
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="guarantee_type_input">担保类型</label>
|
||
<input name="guarantee_type" id="guarantee_type_input" placeholder="担保类型" value="<?= e((string)($editing['guarantee_type'] ?? '')) ?>">
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="guarantor_input">担保人</label>
|
||
<input name="guarantor" id="guarantor_input" placeholder="担保人" value="<?= e((string)($editing['guarantor'] ?? '')) ?>">
|
||
</div>
|
||
</div>
|
||
<div class="form-row-3">
|
||
<div class="form-field-stack">
|
||
<label for="annual_rate_input">年利率</label>
|
||
<input type="number" step="0.01" name="annual_rate" id="annual_rate_input" placeholder="年利率" value="<?= e((string)($editing['annual_rate'] ?? '')) ?>">
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="interest_mode_select">结息方式</label>
|
||
<select name="interest_mode" id="interest_mode_select">
|
||
<option value="" disabled <?= $financeCfg['interest_mode'] === '' ? 'selected' : '' ?> style="color:#9ca3af">请选择</option>
|
||
<option value="monthly" <?= $financeCfg['interest_mode'] === 'monthly' ? 'selected' : '' ?>>按月计息</option>
|
||
<option value="quarterly" <?= $financeCfg['interest_mode'] === 'quarterly' ? 'selected' : '' ?>>按季计息</option>
|
||
<option value="bullet" <?= $financeCfg['interest_mode'] === 'bullet' ? 'selected' : '' ?>>到期一次性付清</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="settlement_day">结息日</label>
|
||
<input type="number" min="1" max="28" name="settlement_day" id="settlement_day" placeholder="1-28" value="<?= (int)$financeCfg['settlement_day'] ?>">
|
||
</div>
|
||
</div>
|
||
<?php
|
||
$endDateVal = (string)($editing['end_date'] ?? '');
|
||
$overdueSelectable = $endDateVal !== '' && $endDateVal < date('Y-m-d');
|
||
$earlySettleSelectable = $endDateVal !== '' && $endDateVal > date('Y-m-d');
|
||
?>
|
||
<div class="form-row-overdue">
|
||
<div class="form-field-stack">
|
||
<label for="is_overdue">是否逾期</label>
|
||
<select name="is_overdue" id="is_overdue" <?= $overdueSelectable ? '' : 'disabled' ?>>
|
||
<option value="0" <?= empty($financeCfg['manual_overdue']) ? 'selected' : '' ?>><?= $overdueSelectable ? '否' : '否(未超到期日不可选)' ?></option>
|
||
<option value="1" <?= !empty($financeCfg['manual_overdue']) ? 'selected' : '' ?>>是</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-field-stack form-col-float">
|
||
<label for="overdue_float_pct">上浮%</label>
|
||
<input type="number" step="0.01" min="0" name="overdue_float_pct" id="overdue_float_pct" value="<?= e((string)$financeCfg['overdue_float_pct']) ?>" title="逾期利息上浮百分比">
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="plan_enabled">是否有分期还款计划</label>
|
||
<select name="plan_enabled" id="plan_enabled">
|
||
<option value="0" <?= empty($planCfg['enabled']) ? 'selected' : '' ?>>否</option>
|
||
<option value="1" <?= !empty($planCfg['enabled']) ? 'selected' : '' ?>>是</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="is_early_settled">是否提前还款</label>
|
||
<select name="is_early_settled" id="is_early_settled" <?= $earlySettleSelectable ? '' : 'disabled' ?>>
|
||
<option value="0" <?= empty($financeCfg['manual_early_settled']) ? 'selected' : '' ?>><?= $earlySettleSelectable ? '否' : '否(仅到期日前可选提前还款/标已完成)' ?></option>
|
||
<option value="1" <?= !empty($financeCfg['manual_early_settled']) ? 'selected' : '' ?>>是(状态设为已完成)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="full form-field-stack">
|
||
<label for="archive_remark">备注</label>
|
||
<textarea name="archive_remark" id="archive_remark" rows="3" placeholder="可填写补充说明、内部备忘等"><?= e((string)($financeCfg['remark'] ?? '')) ?></textarea>
|
||
</div>
|
||
<div class="full" id="plan_wrap" style="<?= empty($planCfg['enabled']) ? 'display:none' : '' ?>">
|
||
<table id="plan_table">
|
||
<tr><th>还款日期</th><th>还款金额</th><th>状态操作</th><th>延期日期</th><th>行操作</th></tr>
|
||
<?php foreach ($planRows as $row): ?>
|
||
<tr>
|
||
<td>
|
||
<input type="hidden" name="plan_id[]" value="<?= e((string)($row['id'] ?? '')) ?>">
|
||
<input type="date" name="plan_due_date[]" value="<?= e((string)($row['due_date'] ?? '')) ?>" onchange="syncPlanAction(this)">
|
||
</td>
|
||
<td><input type="number" step="0.01" name="plan_amount[]" value="<?= e((string)($row['amount'] ?? '')) ?>"></td>
|
||
<td>
|
||
<?php $st = (string)($row['status'] ?? 'pending'); ?>
|
||
<select name="plan_status[]" onchange="syncDelayInput(this)">
|
||
<option value="pending" <?= $st === 'pending' ? 'selected' : '' ?>>未还款</option>
|
||
<option value="paid" <?= $st === 'paid' ? 'selected' : '' ?>><?= (!empty($row['due_date']) && $row['due_date'] > date('Y-m-d')) ? '提前还款' : '按期还款' ?></option>
|
||
<option value="delayed" <?= $st === 'delayed' ? 'selected' : '' ?>>延期</option>
|
||
<option value="overdue" <?= $st === 'overdue' ? 'selected' : '' ?>>逾期</option>
|
||
</select>
|
||
</td>
|
||
<td><input type="date" name="plan_delay_date[]" value="" <?= $st === 'delayed' ? '' : 'disabled' ?>></td>
|
||
<td><button type="button" class="btn-danger" onclick="this.closest('tr').remove()">删除</button></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</table>
|
||
<p><button type="button" class="btn-gray" onclick="addPlanRow()">增加一期</button></p>
|
||
<p class="small muted">说明:到期日前可选“提前还款”;到期日后可选“按期还款/延期/逾期”。逾期未归还计划本金按上浮利率计息,剩余额度按已还款计划与手动归还本金自动计算。</p>
|
||
</div>
|
||
<?php if ($editing): ?>
|
||
<div class="full" style="border-top:1px solid var(--line);padding-top:12px;margin-top:4px">
|
||
<h4 class="section-title" style="margin:0 0 10px;font-size:15px">贷款延期与归还</h4>
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="loan_extend_to">延期新到期日(选填)</label>
|
||
<input type="date" name="loan_extend_to" id="loan_extend_to" value="">
|
||
<p class="small muted" style="margin:0">填写后保存为新的到期日,须不早于当前到期日与起始日。</p>
|
||
</div>
|
||
<?php if ($formKind === ARCHIVE_KIND_BANK_LOAN): ?>
|
||
<div class="form-field-stack">
|
||
<label>已累计归还本金 / 利息(元)</label>
|
||
<p class="small" style="margin:0"><?= e(amount((float)$financeCfg['manual_principal_repaid'])) ?> / <?= e(amount((float)$financeCfg['manual_interest_paid'])) ?></p>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="form-field-stack">
|
||
<label class="muted">对外担保</label>
|
||
<p class="small muted" style="margin:0">可仅使用延期;本金利息归还适用于银行贷款。</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php if ($formKind === ARCHIVE_KIND_BANK_LOAN): ?>
|
||
<div class="form-row-2">
|
||
<div class="form-field-stack">
|
||
<label for="principal_repayment_now">本次归还本金(元)</label>
|
||
<input type="number" step="0.01" min="0" name="principal_repayment_now" id="principal_repayment_now" placeholder="0" value="">
|
||
</div>
|
||
<div class="form-field-stack">
|
||
<label for="interest_repayment_now">本次归还利息(元)</label>
|
||
<input type="number" step="0.01" min="0" name="interest_repayment_now" id="interest_repayment_now" placeholder="0" value="">
|
||
</div>
|
||
</div>
|
||
<p class="small muted" style="margin:0">归还本金从剩余额度中扣减;归还利息计入「已归还利息」,减少当前应付与结息日应付利息。</p>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php if (!$editing): ?>
|
||
<div class="full">
|
||
<label>上传文件(新建时可直接选择)</label>
|
||
<input type="file" name="new_files[]" multiple>
|
||
<p class="small muted">点击“创建”后会自动上传并在编辑页文件管理列表中显示。</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="form-actions">
|
||
<button type="submit"><?= $editing ? '保存' : '创建' ?></button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<?php if ($editArchiveId > 0): ?>
|
||
<div class="card" style="margin-top:12px">
|
||
<h3 class="section-title">文件管理(编辑页)</h3>
|
||
<form method="post" action="index.php?action=upload_file" enctype="multipart/form-data">
|
||
<input type="hidden" name="archive_id" value="<?= $editArchiveId ?>">
|
||
<input type="file" name="files[]" multiple required>
|
||
<button type="submit">上传并刷新列表</button>
|
||
</form>
|
||
<table style="margin-top:8px">
|
||
<tr><th>文件名</th><th>大小</th><th>重命名</th><th>操作</th></tr>
|
||
<?php foreach ($editFiles as $f): ?>
|
||
<tr>
|
||
<td><?= e((string)$f['original_name']) ?></td>
|
||
<td><?= number_format(((int)$f['file_size']) / 1024, 1) ?> KB</td>
|
||
<td>
|
||
<form method="post" action="index.php?action=rename_file" class="inline">
|
||
<input type="hidden" name="file_id" value="<?= (int)$f['id'] ?>">
|
||
<input name="new_name" value="<?= e((string)pathinfo((string)$f['original_name'], PATHINFO_FILENAME)) ?>" placeholder="新文件名" required>
|
||
<button class="btn-gray" type="submit">重命名</button>
|
||
</form>
|
||
</td>
|
||
<td>
|
||
<div class="icon-actions">
|
||
<?php if (canPreview((string)$f['file_ext'])): ?><a class="icon-btn" target="_blank" href="index.php?action=preview_file&id=<?= (int)$f['id'] ?>">👁 预览</a><?php endif; ?>
|
||
<a class="icon-btn" href="index.php?action=download_file&id=<?= (int)$f['id'] ?>">⬇ 下载</a>
|
||
<a class="icon-btn danger" href="index.php?action=delete_file&id=<?= (int)$f['id'] ?>" onclick="return confirm('确认删除?')">🗑 删除</a>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($editFiles)): ?>
|
||
<tr><td colspan="4" class="muted">暂无文件</td></tr>
|
||
<?php endif; ?>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
<script>
|
||
(function(){
|
||
var sel=document.getElementById('plan_enabled');
|
||
var wrap=document.getElementById('plan_wrap');
|
||
function setPlanVisibility(){
|
||
if(sel&&wrap){wrap.style.display=sel.value==='1'?'block':'none';}
|
||
}
|
||
if(sel&&wrap){
|
||
sel.addEventListener('change',setPlanVisibility);
|
||
}
|
||
setPlanVisibility();
|
||
var archiveKind=document.getElementById('archive_kind');
|
||
var loanInstLabel=document.getElementById('loan_institution_label');
|
||
var dirHint=document.getElementById('archive_auto_dir_hint');
|
||
function syncArchiveKindUi(){
|
||
if(!dirHint){return;}
|
||
if(archiveKind&&archiveKind.value==='external_guarantee'){
|
||
dirHint.textContent='未手动选择目录时:将按「对外担保 / 起始日年份 / 担保人」创建或归入目录(三级目录为担保人名称,非借款主体)。下方「债权人」为选填。';
|
||
if(loanInstLabel){loanInstLabel.textContent='债权人(选填)';}
|
||
}else{
|
||
dirHint.textContent='未手动选择目录时:将按「银行贷款 / 起始日年份 / 贷款机构」创建或归入目录。';
|
||
if(loanInstLabel){loanInstLabel.textContent='贷款机构';}
|
||
}
|
||
syncAutoArchiveName();
|
||
}
|
||
if(archiveKind){archiveKind.addEventListener('change',syncArchiveKindUi);}
|
||
syncArchiveKindUi();
|
||
document.querySelectorAll('input[name="plan_due_date[]"]').forEach(function(x){syncPlanAction(x);});
|
||
document.querySelectorAll('select[name="plan_status[]"]').forEach(function(x){syncDelayInput(x);});
|
||
var interestMode=document.querySelector('select[name="interest_mode"]');
|
||
var settlementDay=document.getElementById('settlement_day');
|
||
var borrowerInput=document.getElementById('borrower_input');
|
||
var creditInput=document.getElementById('credit_limit_input');
|
||
var archiveNameAuto=document.getElementById('archive_name_auto');
|
||
var guarantorInput=document.getElementById('guarantor_input');
|
||
var institutionSelect=document.getElementById('loan_institution_select');
|
||
var institutionsJson=document.getElementById('loan_institutions_json');
|
||
var loanEndDate=document.getElementById('loan_end_date');
|
||
var isOverdue=document.getElementById('is_overdue');
|
||
var isEarlySettled=document.getElementById('is_early_settled');
|
||
var overduePct=document.getElementById('overdue_float_pct');
|
||
function syncInterestMode(){
|
||
if(!interestMode||!settlementDay){return;}
|
||
if(interestMode.value==='bullet'){
|
||
settlementDay.value='';
|
||
settlementDay.disabled=true;
|
||
settlementDay.placeholder='到期一次性付息';
|
||
}else{
|
||
settlementDay.disabled=false;
|
||
if(!settlementDay.value){settlementDay.value='21';}
|
||
settlementDay.placeholder=interestMode.value==='quarterly'?'每季几号(1-28)':'每月几号(1-28)';
|
||
}
|
||
}
|
||
if(interestMode){interestMode.addEventListener('change',syncInterestMode);}
|
||
syncInterestMode();
|
||
function syncOverdueAvailability(){
|
||
if(!loanEndDate||!isOverdue){return;}
|
||
var endDate=loanEndDate.value;
|
||
var today=new Date().toISOString().slice(0,10);
|
||
var canSelect=(endDate && endDate<today);
|
||
if(!canSelect){
|
||
isOverdue.value='0';
|
||
isOverdue.disabled=true;
|
||
if(isOverdue.options.length>0){isOverdue.options[0].text='否(未超到期日不可选)';}
|
||
}else{
|
||
isOverdue.disabled=false;
|
||
if(isOverdue.options.length>0 && isOverdue.options[0].value==='0'){isOverdue.options[0].text='否';}
|
||
}
|
||
}
|
||
if(loanEndDate){loanEndDate.addEventListener('change',syncOverdueAvailability);}
|
||
syncOverdueAvailability();
|
||
function syncEarlySettleAvailability(){
|
||
if(!loanEndDate||!isEarlySettled){return;}
|
||
var endDate=loanEndDate.value;
|
||
var today=new Date().toISOString().slice(0,10);
|
||
var canSelect=(endDate && endDate>today);
|
||
if(!canSelect){
|
||
isEarlySettled.value='0';
|
||
isEarlySettled.disabled=true;
|
||
if(isEarlySettled.options.length>0){isEarlySettled.options[0].text='否(仅到期日前可选)';}
|
||
}else{
|
||
isEarlySettled.disabled=false;
|
||
if(isEarlySettled.options.length>0 && isEarlySettled.options[0].value==='0'){isEarlySettled.options[0].text='否';}
|
||
}
|
||
}
|
||
function syncMutualStatusSelection(){
|
||
if(!isOverdue||!isEarlySettled){return;}
|
||
if(isEarlySettled.value==='1'){
|
||
isOverdue.value='0';
|
||
isOverdue.disabled=true;
|
||
}else{
|
||
syncOverdueAvailability();
|
||
}
|
||
}
|
||
if(loanEndDate){loanEndDate.addEventListener('change',syncEarlySettleAvailability);}
|
||
if(isEarlySettled){isEarlySettled.addEventListener('change',syncMutualStatusSelection);}
|
||
syncEarlySettleAvailability();
|
||
syncMutualStatusSelection();
|
||
|
||
function syncAutoArchiveName(){
|
||
if(!creditInput||!archiveNameAuto||!archiveKind){return;}
|
||
var c=parseFloat(creditInput.value||'');
|
||
if(isNaN(c)){ archiveNameAuto.value=''; return; }
|
||
var wan=(c/10000).toFixed(2).replace(/\.00$/,'').replace(/(\.\d)0$/,'$1');
|
||
if(archiveKind.value==='external_guarantee'){
|
||
if(!guarantorInput){return;}
|
||
var g=(guarantorInput.value||'').trim();
|
||
if(!g){ archiveNameAuto.value=''; return; }
|
||
archiveNameAuto.value=g+'对外担保'+wan+'万元';
|
||
}else{
|
||
if(!borrowerInput){return;}
|
||
var b=(borrowerInput.value||'').trim();
|
||
if(!b){ archiveNameAuto.value=''; return; }
|
||
archiveNameAuto.value=b+wan+'万元';
|
||
}
|
||
}
|
||
if(borrowerInput){borrowerInput.addEventListener('input',syncAutoArchiveName);}
|
||
if(guarantorInput){guarantorInput.addEventListener('input',syncAutoArchiveName);}
|
||
if(creditInput){creditInput.addEventListener('input',syncAutoArchiveName);}
|
||
syncAutoArchiveName();
|
||
|
||
function syncInstitutionsJson(){
|
||
if(!institutionSelect||!institutionsJson){return;}
|
||
var arr=[...institutionSelect.options].map(function(o){return o.value;}).filter(function(v){return v!=='';});
|
||
institutionsJson.value=JSON.stringify(arr);
|
||
}
|
||
window.addInstitutionOption=function(){
|
||
if(!institutionSelect){return;}
|
||
var v=prompt('请输入贷款机构名称');
|
||
if(!v){return;}
|
||
v=v.trim();
|
||
if(!v){return;}
|
||
var exists=[...institutionSelect.options].some(function(o){return o.value===v;});
|
||
if(!exists){
|
||
var op=document.createElement('option');
|
||
op.value=v;op.textContent=v;
|
||
institutionSelect.appendChild(op);
|
||
}
|
||
institutionSelect.value=v;
|
||
syncInstitutionsJson();
|
||
};
|
||
window.removeInstitutionOption=function(){
|
||
if(!institutionSelect){return;}
|
||
var v=institutionSelect.value;
|
||
if(!v){return;}
|
||
if(!confirm('确认删除当前机构选项?')){return;}
|
||
[...institutionSelect.options].forEach(function(o){if(o.value===v){o.remove();}});
|
||
institutionSelect.value='';
|
||
syncInstitutionsJson();
|
||
};
|
||
if(institutionSelect){institutionSelect.addEventListener('change',syncInstitutionsJson);}
|
||
syncInstitutionsJson();
|
||
})();
|
||
function syncPlanAction(el){
|
||
var tr=el.closest('tr'); if(!tr){return;}
|
||
var sel=tr.querySelector('select[name="plan_status[]"]');
|
||
if(!sel){return;}
|
||
var due=el.value;
|
||
var today=new Date().toISOString().slice(0,10);
|
||
var paidOpt=sel.querySelector('option[value="paid"]');
|
||
if(paidOpt){paidOpt.textContent=(due && due>today)?'提前还款':'按期还款';}
|
||
var delayedOpt=sel.querySelector('option[value="delayed"]');
|
||
var overdueOpt=sel.querySelector('option[value="overdue"]');
|
||
var canAfterDue=(due && due<=today);
|
||
if(delayedOpt){
|
||
delayedOpt.disabled=!canAfterDue;
|
||
if(!canAfterDue && sel.value==='delayed'){sel.value='pending';}
|
||
}
|
||
if(overdueOpt){
|
||
overdueOpt.disabled=!canAfterDue;
|
||
if(!canAfterDue && sel.value==='overdue'){sel.value='pending';}
|
||
}
|
||
syncDelayInput(sel);
|
||
}
|
||
function syncDelayInput(el){
|
||
var tr=el.closest('tr'); if(!tr){return;}
|
||
var d=tr.querySelector('input[name="plan_delay_date[]"]');
|
||
if(!d){return;}
|
||
d.disabled=(el.value!=='delayed');
|
||
if(d.disabled){d.value='';}
|
||
}
|
||
function addPlanRow(){
|
||
var tb=document.getElementById('plan_table');
|
||
if(!tb){return;}
|
||
var tr=document.createElement('tr');
|
||
tr.innerHTML='<td><input type="hidden" name="plan_id[]" value=""><input type="date" name="plan_due_date[]" onchange="syncPlanAction(this)"></td><td><input type="number" step="0.01" name="plan_amount[]"></td><td><select name="plan_status[]" onchange="syncDelayInput(this)"><option value="pending">未还款</option><option value="paid">提前还款</option><option value="delayed">延期</option><option value="overdue">逾期</option></select></td><td><input type="date" name="plan_delay_date[]" disabled></td><td><button type="button" class="btn-danger" onclick="this.closest(\'tr\').remove()">删除</button></td>';
|
||
tb.appendChild(tr);
|
||
}
|
||
</script>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'users') {
|
||
requireAdmin();
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$do = $_POST['do'] ?? '';
|
||
if ($do === 'create') {
|
||
$u = trim((string)$_POST['username']);
|
||
$p = (string)$_POST['password'];
|
||
if (!preg_match('/^[a-zA-Z0-9_]{4,20}$/', $u) || strlen($p) < 6 || strlen($p) > 20) {
|
||
flash('error', '用户名或密码格式不正确');
|
||
} else {
|
||
try {
|
||
$pdo->prepare('INSERT INTO users(username,password_hash,role,created_at) VALUES(:u,:p,:r,:c)')
|
||
->execute([':u' => $u, ':p' => password_hash($p, PASSWORD_DEFAULT), ':r' => 'user', ':c' => now()]);
|
||
flash('ok', '用户已创建');
|
||
} catch (Throwable $e) {
|
||
flash('error', '用户名已存在');
|
||
}
|
||
}
|
||
} elseif ($do === 'role') {
|
||
$uid = (int)$_POST['uid'];
|
||
$role = $_POST['role'] === 'admin' ? 'admin' : 'user';
|
||
$pdo->prepare('UPDATE users SET role=:r WHERE id=:id')->execute([':r' => $role, ':id' => $uid]);
|
||
flash('ok', '角色已更新');
|
||
} elseif ($do === 'reset') {
|
||
$uid = (int)$_POST['uid'];
|
||
$np = (string)$_POST['new_password'];
|
||
if (strlen($np) < 6 || strlen($np) > 20) {
|
||
flash('error', '密码长度需为6-20');
|
||
} else {
|
||
$pdo->prepare('UPDATE users SET password_hash=:p WHERE id=:id')->execute([':p' => password_hash($np, PASSWORD_DEFAULT), ':id' => $uid]);
|
||
flash('ok', '密码已重置');
|
||
}
|
||
} elseif ($do === 'delete') {
|
||
$uid = (int)$_POST['uid'];
|
||
if ($uid === (int)$me['id']) {
|
||
flash('error', '不能删除当前登录账号');
|
||
} else {
|
||
$pdo->prepare('DELETE FROM users WHERE id=:id')->execute([':id' => $uid]);
|
||
flash('ok', '用户已删除');
|
||
}
|
||
}
|
||
header('Location: index.php?action=users');
|
||
exit;
|
||
}
|
||
$users = $pdo->query('SELECT id,username,role,created_at FROM users ORDER BY id ASC')->fetchAll();
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3>用户管理</h3>
|
||
<form method="post" style="margin-bottom:12px">
|
||
<input type="hidden" name="do" value="create">
|
||
<input name="username" placeholder="用户名(4-20位字母数字下划线)" required>
|
||
<input name="password" placeholder="初始密码(6-20位)" required>
|
||
<button type="submit">创建用户</button>
|
||
</form>
|
||
<table>
|
||
<tr><th>ID</th><th>用户名</th><th>角色</th><th>创建时间</th><th>操作</th></tr>
|
||
<?php foreach ($users as $u): ?>
|
||
<tr>
|
||
<td><?= (int)$u['id'] ?></td><td><?= e(maskUsername((string)$u['username'])) ?></td>
|
||
<td>
|
||
<form method="post" class="inline">
|
||
<input type="hidden" name="do" value="role">
|
||
<input type="hidden" name="uid" value="<?= (int)$u['id'] ?>">
|
||
<select name="role" onchange="this.form.submit()">
|
||
<option value="user" <?= $u['role'] === 'user' ? 'selected' : '' ?>>普通用户</option>
|
||
<option value="admin" <?= $u['role'] === 'admin' ? 'selected' : '' ?>>管理员</option>
|
||
</select>
|
||
</form>
|
||
</td>
|
||
<td><?= e($u['created_at']) ?></td>
|
||
<td>
|
||
<form method="post" class="inline">
|
||
<input type="hidden" name="do" value="reset">
|
||
<input type="hidden" name="uid" value="<?= (int)$u['id'] ?>">
|
||
<input name="new_password" placeholder="新密码" required>
|
||
<button class="btn-gray" type="submit">重置密码</button>
|
||
</form>
|
||
<form method="post" class="inline" onsubmit="return confirm('确认删除用户?')">
|
||
<input type="hidden" name="do" value="delete">
|
||
<input type="hidden" name="uid" value="<?= (int)$u['id'] ?>">
|
||
<button class="btn-danger" type="submit">删除</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</table>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'profile') {
|
||
requireLogin();
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$cur = (string)($_POST['current_password'] ?? '');
|
||
$new = (string)($_POST['new_password'] ?? '');
|
||
$confirm = (string)($_POST['confirm_password'] ?? '');
|
||
$stmt = $pdo->prepare('SELECT * FROM users WHERE id=:id');
|
||
$stmt->execute([':id' => (int)$me['id']]);
|
||
$u = $stmt->fetch();
|
||
if (!$u || !password_verify($cur, $u['password_hash'])) {
|
||
flash('error', '当前密码错误');
|
||
} elseif (strlen($new) < 6 || strlen($new) > 20) {
|
||
flash('error', '新密码长度需为6-20位');
|
||
} elseif ($new !== $confirm) {
|
||
flash('error', '新密码与确认密码不一致');
|
||
} else {
|
||
$pdo->prepare('UPDATE users SET password_hash=:p WHERE id=:id')->execute([':p' => password_hash($new, PASSWORD_DEFAULT), ':id' => (int)$me['id']]);
|
||
flash('ok', '密码修改成功');
|
||
}
|
||
header('Location: index.php?action=profile');
|
||
exit;
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3>个人设置 - 修改密码</h3>
|
||
<form method="post">
|
||
<p><input type="password" name="current_password" placeholder="当前密码" required></p>
|
||
<p><input type="password" name="new_password" placeholder="新密码(6-20位)" required></p>
|
||
<p><input type="password" name="confirm_password" placeholder="确认新密码" required></p>
|
||
<button type="submit">保存修改</button>
|
||
<a href="index.php">返回</a>
|
||
</form>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'quick_queries') {
|
||
requireLogin();
|
||
pageHeader($me); ?>
|
||
<div class="card fin-tool-page">
|
||
<h3 class="section-title">便捷查询</h3>
|
||
<?= quickQueryMenuHtml() ?>
|
||
<p class="small muted" style="margin-top:12px">应付本息:统计<strong>履行中及逾期中</strong>银行贷款按机构当前应付利息、本结息周期整段结息日应付利息,以及当月还款计划金额。</p>
|
||
<p class="small muted">对外担保:按主体名称检索“该主体作为担保人”的<strong>履行中及逾期中</strong>银行贷款与对外担保档案。</p>
|
||
<p class="small muted">查询总金额:按选项汇总<strong>履行中及逾期中</strong>档案的授信总额与剩余额度合计。</p>
|
||
<p class="small muted">利息计算器 / 承兑贴现计算器:本地浏览器计算,结果仅供参考。</p>
|
||
<p class="small muted">历史还款记录:汇总分期计划中「已还款」本金及状态为「已完成」档案的授信本金规模。</p>
|
||
<p style="margin-top:12px"><a class="clear-link" href="index.php">返回首页</a></p>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'repayment_history') {
|
||
requireLogin();
|
||
$rowsAll = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
|
||
$planAll = loadRepaymentPlans();
|
||
$financeAll = loadFinanceConfigs();
|
||
$hr = qqBuildRepaymentHistory($rowsAll, $planAll, $financeAll);
|
||
$planRows = $hr['plan_rows'];
|
||
$cb = $hr['completed_bank'];
|
||
$cg = $hr['completed_guar'];
|
||
pageHeader($me); ?>
|
||
<div class="card fin-tool-page">
|
||
<h3 class="section-title">历史还款记录查询</h3>
|
||
<div class="quick-links-col" style="margin-bottom:14px">
|
||
<a class="tree-link" href="index.php?action=quick_queries">← 返回便捷查询</a>
|
||
</div>
|
||
<div class="fin-panel" style="margin-bottom:14px">
|
||
<h4 style="margin:0 0 8px">汇总</h4>
|
||
<table>
|
||
<tr><th>项目</th><th>笔数</th><th>金额合计(元)</th></tr>
|
||
<tr>
|
||
<td>分期还款计划中「已还款」本金</td>
|
||
<td><?= count($planRows) ?></td>
|
||
<td><strong><?= e(amount((float)$hr['plan_paid_total'])) ?></strong></td>
|
||
</tr>
|
||
<tr>
|
||
<td>已完成 — 银行贷款(授信本金规模)</td>
|
||
<td><?= (int)$cb['count'] ?></td>
|
||
<td><strong><?= e(amount((float)$cb['sum_credit'])) ?></strong></td>
|
||
</tr>
|
||
<tr>
|
||
<td>已完成 — 对外担保(授信本金规模)</td>
|
||
<td><?= (int)$cg['count'] ?></td>
|
||
<td><strong><?= e(amount((float)$cg['sum_credit'])) ?></strong></td>
|
||
</tr>
|
||
</table>
|
||
<p class="fin-muted" style="margin:10px 0 0">「已完成」指合同状态为已完成;授信本金规模为档案授信额度合计,用于对应到期/结清贷款本金规模参考。分期已还与整笔结清可能同时存在,请勿简单相加避免重复理解。</p>
|
||
</div>
|
||
<h4 class="section-title" style="font-size:16px">分期计划已还款明细</h4>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<tr><th>档案ID</th><th>档案名称</th><th>机构</th><th>借款主体</th><th>计划到期日</th><th>标记还款日</th><th>还款金额</th><th>操作</th></tr>
|
||
<?php foreach ($planRows as $pr): ?>
|
||
<tr>
|
||
<td><?= (int)$pr['archive_id'] ?></td>
|
||
<td><?= e((string)$pr['archive_name']) ?></td>
|
||
<td><?= e((string)$pr['loan_institution']) ?></td>
|
||
<td><?= e((string)$pr['borrower']) ?></td>
|
||
<td><?= e((string)$pr['due_date']) ?></td>
|
||
<td><?= e((string)$pr['paid_at']) ?></td>
|
||
<td><?= e(amount((float)$pr['amount'])) ?></td>
|
||
<td><?php if ((int)$pr['archive_id'] > 0): ?><a class="icon-btn" href="index.php?action=archive_detail&id=<?= (int)$pr['archive_id'] ?>">查看</a><?php else: ?>—<?php endif; ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($planRows)): ?><tr><td colspan="8" class="muted">暂无已标记还款的分期计划</td></tr><?php endif; ?>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'interest_calculator') {
|
||
requireLogin();
|
||
pageHeader($me);
|
||
require __DIR__ . '/../app/views/interest_calculator.php';
|
||
pageFooter();
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'acceptance_discount_calculator') {
|
||
requireLogin();
|
||
pageHeader($me);
|
||
require __DIR__ . '/../app/views/acceptance_discount_calculator.php';
|
||
pageFooter();
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'query_totals') {
|
||
requireLogin();
|
||
$scope = (string)($_GET['scope'] ?? 'all_bank');
|
||
$inst = trim((string)($_GET['inst'] ?? ''));
|
||
$allowedScopes = ['all_bank', 'all_guar', 'bank_inst', 'guar_inst'];
|
||
if (!in_array($scope, $allowedScopes, true)) {
|
||
$scope = 'all_bank';
|
||
}
|
||
$rowsAll = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
|
||
$financeAll = loadFinanceConfigs();
|
||
$tot = qqBuildTotalsQuery($rowsAll, $financeAll, $scope, $inst !== '' ? $inst : null);
|
||
$loanInstitutions = loadLoanInstitutions();
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3 class="section-title">查询总金额</h3>
|
||
<div class="quick-links-col" style="margin-bottom:12px">
|
||
<a class="tree-link" href="index.php?action=quick_queries">← 返回便捷查询</a>
|
||
</div>
|
||
<form method="get" class="toolbar-left" style="flex-wrap:wrap;gap:10px;align-items:flex-end">
|
||
<input type="hidden" name="action" value="query_totals">
|
||
<div class="form-field-stack" style="min-width:200px">
|
||
<label for="qt_scope">统计范围</label>
|
||
<select name="scope" id="qt_scope">
|
||
<option value="all_bank" <?= $scope === 'all_bank' ? 'selected' : '' ?>>履行中及逾期中 — 全部银行贷款</option>
|
||
<option value="all_guar" <?= $scope === 'all_guar' ? 'selected' : '' ?>>履行中及逾期中 — 全部对外担保</option>
|
||
<option value="bank_inst" <?= $scope === 'bank_inst' ? 'selected' : '' ?>>履行中及逾期中 — 指定机构的银行贷款</option>
|
||
<option value="guar_inst" <?= $scope === 'guar_inst' ? 'selected' : '' ?>>履行中及逾期中 — 指定机构的对外担保</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-field-stack" id="qt_inst_wrap" style="min-width:220px;<?= ($scope === 'bank_inst' || $scope === 'guar_inst') ? '' : 'opacity:.45' ?>">
|
||
<label for="qt_inst">机构名称(后两项必填)</label>
|
||
<input name="inst" id="qt_inst" list="qt_inst_list" value="<?= e($inst) ?>" placeholder="与档案中贷款机构/债权人一致">
|
||
<datalist id="qt_inst_list">
|
||
<?php foreach ($loanInstitutions as $li): ?><option value="<?= e($li) ?>"><?php endforeach; ?>
|
||
</datalist>
|
||
</div>
|
||
<button type="submit">查询</button>
|
||
</form>
|
||
<script>
|
||
(function(){
|
||
var sel=document.getElementById('qt_scope');
|
||
var wrap=document.getElementById('qt_inst_wrap');
|
||
function sync(){if(!sel||!wrap)return;var need=sel.value==='bank_inst'||sel.value==='guar_inst';wrap.style.opacity=need?'1':'.45';}
|
||
if(sel){sel.addEventListener('change',sync);}sync();
|
||
})();
|
||
</script>
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<h3 class="section-title">汇总结果</h3>
|
||
<table>
|
||
<tr><th>笔数(履行+逾期)</th><th>授信总额合计</th><th>剩余额度合计</th></tr>
|
||
<tr>
|
||
<td><?= (int)($tot['count'] ?? 0) ?></td>
|
||
<td><?= e(amount((float)($tot['sum_credit'] ?? 0))) ?></td>
|
||
<td><?= e(amount((float)($tot['sum_remaining'] ?? 0))) ?></td>
|
||
</tr>
|
||
</table>
|
||
<p class="small muted" style="margin-top:10px">统计范围含状态为「履行中」与「逾期中」的档案。授信总额、剩余额度均来自各档案字段;剩余额度含分期已还与手动归还本金后的结果。</p>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'query_payable_interest') {
|
||
requireLogin();
|
||
$exportFormat = (string)($_GET['format'] ?? '');
|
||
$rowsAll = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
|
||
$financeAll = loadFinanceConfigs();
|
||
$planAll = loadRepaymentPlans();
|
||
$quickStats = qqBuildPayableInterestData($rowsAll, $financeAll, $planAll);
|
||
$instRows = $quickStats['instRows'];
|
||
$planRows = $quickStats['planRows'];
|
||
$planTotal = (float)($quickStats['planTotal'] ?? 0);
|
||
|
||
if ($exportFormat === 'xlsx' && class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
|
||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||
$sheet1 = $spreadsheet->getActiveSheet();
|
||
$sheet1->setTitle('机构利息汇总');
|
||
$sheet1->fromArray(['机构', '笔数(履行+逾期)', '当前应付利息', '结息日', '结息日应付利息'], null, 'A1');
|
||
$sheet1->fromArray(array_map(fn($x) => [$x['loan_institution'], $x['loan_count'], amount((float)$x['current_interest']), (string)($x['settlement_date'] ?? ''), amount((float)$x['settlement_interest'])], $instRows), null, 'A2');
|
||
foreach (range('A', 'E') as $col) { $sheet1->getColumnDimension($col)->setWidth(24); }
|
||
|
||
$sheet2 = $spreadsheet->createSheet();
|
||
$sheet2->setTitle('当月还款计划');
|
||
$sheet2->fromArray(['机构', '档案名称', '借款主体', '到期日', '金额', '状态'], null, 'A1');
|
||
$sheet2->fromArray(array_map(fn($x) => [$x['loan_institution'], $x['archive_name'], $x['borrower'], $x['due_date'], amount((float)$x['amount']), $x['status']], $planRows), null, 'A2');
|
||
$sheet2->setCellValue('A' . (count($planRows) + 3), '合计');
|
||
$sheet2->setCellValue('E' . (count($planRows) + 3), amount($planTotal));
|
||
foreach (range('A', 'F') as $col) { $sheet2->getColumnDimension($col)->setWidth(22); }
|
||
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode('应付本息查询_' . date('Y-m-d') . '.xlsx') . '"');
|
||
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');
|
||
exit;
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3 class="section-title">应付本息查询</h3>
|
||
<div class="quick-links-col" style="margin-bottom:10px">
|
||
<a class="tree-link" href="index.php?action=quick_queries">← 返回便捷查询</a>
|
||
<a class="tree-link" href="index.php?action=query_totals">查询总金额</a>
|
||
<a class="toolbar-btn" href="index.php?action=query_payable_interest&format=xlsx" style="margin-top:4px">导出xlsx</a>
|
||
</div>
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<h3 class="section-title">一、银行贷款机构利息统计(履行中及逾期中)</h3>
|
||
<table>
|
||
<tr><th>机构</th><th>笔数</th><th>当前应付利息</th><th>结息日</th><th title="本结息周期整段应计,非自今日至结息日">结息日应付利息</th></tr>
|
||
<?php foreach ($instRows as $r): ?>
|
||
<tr>
|
||
<td><?= e((string)$r['loan_institution']) ?></td>
|
||
<td><?= (int)$r['loan_count'] ?></td>
|
||
<td><?= e(amount((float)$r['current_interest'])) ?></td>
|
||
<td><?= e((string)($r['settlement_date'] ?? '')) ?></td>
|
||
<td><?= e(amount((float)$r['settlement_interest'])) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (!empty($instRows)): ?>
|
||
<?php
|
||
$sumCur = 0.0;
|
||
$sumSettle = 0.0;
|
||
foreach ($instRows as $x) {
|
||
$sumCur += (float)$x['current_interest'];
|
||
$sumSettle += (float)$x['settlement_interest'];
|
||
}
|
||
?>
|
||
<tr><td><strong>合计</strong></td><td>-</td><td><strong><?= e(amount($sumCur)) ?></strong></td><td>-</td><td><strong><?= e(amount($sumSettle)) ?></strong></td></tr>
|
||
<?php else: ?><tr><td colspan="5" class="muted">暂无数据</td></tr><?php endif; ?>
|
||
</table>
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<h3 class="section-title">二、当月还款计划金额</h3>
|
||
<div class="table-wrap" style="margin-top:10px">
|
||
<table>
|
||
<tr><th>机构</th><th>档案名称</th><th>借款主体</th><th>到期日</th><th>金额</th><th>状态</th></tr>
|
||
<?php foreach ($planRows as $r): ?>
|
||
<tr>
|
||
<td><?= e((string)$r['loan_institution']) ?></td>
|
||
<td><?= e((string)$r['archive_name']) ?></td>
|
||
<td><?= e((string)$r['borrower']) ?></td>
|
||
<td><?= e((string)$r['due_date']) ?></td>
|
||
<td><?= e(amount((float)$r['amount'])) ?></td>
|
||
<td><?= e((string)$r['status']) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (!empty($planRows)): ?><tr><td><strong>合计</strong></td><td colspan="3">-</td><td><strong><?= e(amount($planTotal)) ?></strong></td><td>-</td></tr><?php endif; ?>
|
||
<?php if (empty($planRows)): ?><tr><td colspan="6" class="muted">暂无当月还款计划</td></tr><?php endif; ?>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'query_external_guarantee') {
|
||
requireLogin();
|
||
$kw = trim((string)($_GET['kw'] ?? ''));
|
||
$format = (string)($_GET['format'] ?? '');
|
||
$rows = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
|
||
$financeAll = loadFinanceConfigs();
|
||
$resultRows = array_values(array_filter(qqSearchGuarantorArchives($rows, $kw), function ($r) use ($financeAll) {
|
||
return archiveStatusIsOngoingAggregate((string)($r['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$r['id']));
|
||
}));
|
||
if ($format === 'xlsx' && $kw !== '' && class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
|
||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$sheet->setTitle('对外担保查询');
|
||
$sheet->fromArray(['档案类型', '档案名称', '贷款机构/债权人', '借款主体', '担保人', '授信额度', '剩余额度', '到期日', '状态'], null, 'A1');
|
||
$sheet->fromArray(array_map(function ($r) use ($financeAll) {
|
||
return [
|
||
archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))),
|
||
(string)($r['name'] ?? ''),
|
||
(string)($r['loan_institution'] ?? ''),
|
||
(string)($r['borrower'] ?? ''),
|
||
(string)($r['guarantor'] ?? ''),
|
||
amount($r['credit_limit'] !== null ? (float)$r['credit_limit'] : null),
|
||
amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null),
|
||
(string)($r['end_date'] ?? ''),
|
||
archiveStatusWithConfig((string)($r['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$r['id'])),
|
||
];
|
||
}, $resultRows), null, 'A2');
|
||
foreach (range('A', 'I') as $col) { $sheet->getColumnDimension($col)->setWidth(18); }
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode('对外担保查询_' . date('Y-m-d') . '.xlsx') . '"');
|
||
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');
|
||
exit;
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3 class="section-title">对外担保查询</h3>
|
||
<div class="quick-links-col" style="margin-bottom:10px">
|
||
<a class="tree-link" href="index.php?action=quick_queries">← 返回便捷查询</a>
|
||
<a class="tree-link" href="index.php?action=query_totals">查询总金额</a>
|
||
</div>
|
||
<form method="get" class="toolbar-left">
|
||
<input type="hidden" name="action" value="query_external_guarantee">
|
||
<input name="kw" value="<?= e($kw) ?>" placeholder="输入主体名称(匹配担保人)" required>
|
||
<button type="submit">查询</button>
|
||
<?php if ($kw !== ''): ?><a class="toolbar-btn" href="index.php?action=query_external_guarantee&kw=<?= urlencode($kw) ?>&format=xlsx">导出xlsx</a><?php endif; ?>
|
||
</form>
|
||
<p class="small muted" style="margin-top:8px">仅列出状态为「履行中」或「逾期中」的档案。</p>
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<div class="table-wrap"><table>
|
||
<tr><th>档案类型</th><th>档案名称</th><th>贷款机构/债权人</th><th>借款主体</th><th>担保人</th><th>授信额度</th><th>剩余额度</th><th>到期日</th><th>状态</th></tr>
|
||
<?php foreach ($resultRows as $r): ?>
|
||
<?php $st = archiveStatusWithConfig((string)($r['end_date'] ?? ''), getArchiveFinanceConfig($financeAll, (int)$r['id'])); ?>
|
||
<tr>
|
||
<td><?= e(archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? '')))) ?></td>
|
||
<td><?= e((string)($r['name'] ?? '')) ?></td>
|
||
<td><?= e((string)($r['loan_institution'] ?? '')) ?></td>
|
||
<td><?= e((string)($r['borrower'] ?? '')) ?></td>
|
||
<td><?= e((string)($r['guarantor'] ?? '')) ?></td>
|
||
<td><?= e(amount($r['credit_limit'] !== null ? (float)$r['credit_limit'] : null)) ?></td>
|
||
<td><?= e(amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null)) ?></td>
|
||
<td><?= e((string)($r['end_date'] ?? '')) ?></td>
|
||
<td><?= e($st) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($resultRows)): ?><tr><td colspan="9" class="muted"><?= $kw === '' ? '请输入主体名称后查询' : '未命中履行中或逾期中的相关档案(已完成等已排除)' ?></td></tr><?php endif; ?>
|
||
</table></div>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
if ($action === 'export') {
|
||
requireLogin();
|
||
$mode = $_GET['mode'] ?? 'all';
|
||
$format = $_GET['format'] ?? 'xlsx';
|
||
$kindFilter = (string)($_GET['kind'] ?? 'bank_loan');
|
||
if (!in_array($kindFilter, ['all', ARCHIVE_KIND_BANK_LOAN, ARCHIVE_KIND_EXTERNAL_GUARANTEE], true)) {
|
||
$kindFilter = ARCHIVE_KIND_BANK_LOAN;
|
||
}
|
||
$selectedRaw = $_GET['ids'] ?? [];
|
||
if (!is_array($selectedRaw)) {
|
||
$selectedRaw = explode(',', (string)$selectedRaw);
|
||
}
|
||
$selected = array_values(array_filter(array_map('intval', $selectedRaw)));
|
||
$dirId = (int)($_GET['dir_id'] ?? 0);
|
||
$dirs = fetchDirectories($pdo);
|
||
$dirFilter = [];
|
||
if ($dirId > 0) {
|
||
$dirFilter = collectSubDirIds($dirs, $dirId);
|
||
}
|
||
$rows = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
|
||
$financeAll = loadFinanceConfigs();
|
||
$rows = array_values(array_filter($rows, function ($r) use ($mode, $selected, $dirFilter, $financeAll, $kindFilter) {
|
||
if ($dirFilter && !in_array((int)$r['directory_id'], $dirFilter, true)) {
|
||
return false;
|
||
}
|
||
$rowKind = normalizeArchiveKind((string)($r['archive_kind'] ?? ''));
|
||
if ($kindFilter !== 'all' && $rowKind !== $kindFilter) {
|
||
return false;
|
||
}
|
||
$status = archiveStatusWithConfig($r['end_date'], getArchiveFinanceConfig($financeAll, (int)$r['id']));
|
||
if ($mode === 'selected' && !in_array((int)$r['id'], $selected, true)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}));
|
||
$modeName = ['all' => '全部', 'selected' => '选中'][$mode] ?? '全部';
|
||
$headers = ['档案类型', '贷款机构', '借款主体', '授信额度', '剩余额度', '起止日', '到期日', '年利率', '担保类型', '担保人', '合同状态'];
|
||
$dataRows = [];
|
||
foreach ($rows as $r) {
|
||
$dataRows[] = [
|
||
archiveKindLabel(normalizeArchiveKind($r['archive_kind'] ?? '')),
|
||
$r['loan_institution'],
|
||
$r['borrower'],
|
||
amount($r['credit_limit'] !== null ? (float)$r['credit_limit'] : null),
|
||
amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null),
|
||
$r['start_date'],
|
||
$r['end_date'],
|
||
$r['annual_rate'] === null ? '' : ((string)$r['annual_rate'] . '%'),
|
||
$r['guarantee_type'],
|
||
$r['guarantor'],
|
||
archiveStatusWithConfig($r['end_date'], getArchiveFinanceConfig($financeAll, (int)$r['id'])),
|
||
];
|
||
}
|
||
|
||
$xlsxEnabled = class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx');
|
||
if ($format === 'xlsx' && $xlsxEnabled) {
|
||
$filename = '档案汇总_' . $modeName . '_' . date('Y-m-d') . '.xlsx';
|
||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$sheet->fromArray($headers, null, 'A1');
|
||
$sheet->fromArray($dataRows, null, 'A2');
|
||
foreach (range('A', 'K') as $col) {
|
||
$sheet->getColumnDimension($col)->setWidth(16);
|
||
}
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
|
||
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
|
||
$writer->save('php://output');
|
||
exit;
|
||
}
|
||
|
||
$filename = '档案汇总_' . $modeName . '_' . date('Y-m-d') . '.csv';
|
||
header('Content-Type: text/csv; charset=UTF-8');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
|
||
echo "\xEF\xBB\xBF";
|
||
$out = fopen('php://output', 'wb');
|
||
fputcsv($out, $headers);
|
||
foreach ($dataRows as $row) {
|
||
fputcsv($out, $row);
|
||
}
|
||
fclose($out);
|
||
exit;
|
||
}
|
||
|
||
$dirs = fetchDirectories($pdo);
|
||
$dirId = (int)($_GET['dir_id'] ?? 0);
|
||
$q = trim((string)($_GET['q'] ?? ''));
|
||
$searchField = $_GET['search_field'] ?? 'all';
|
||
$statusFilter = $_GET['status'] ?? 'not_done';
|
||
$kindFilter = (string)($_GET['kind'] ?? ARCHIVE_KIND_BANK_LOAN);
|
||
if (!in_array($kindFilter, ['all', ARCHIVE_KIND_BANK_LOAN, ARCHIVE_KIND_EXTERNAL_GUARANTEE], true)) {
|
||
$kindFilter = ARCHIVE_KIND_BANK_LOAN;
|
||
}
|
||
$sortBy = $_GET['sort_by'] ?? 'end_date';
|
||
$sortOrder = strtolower((string)($_GET['sort_order'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
|
||
$rows = $pdo->query('SELECT * FROM archives ORDER BY id DESC')->fetchAll();
|
||
$financeAllGlobal = loadFinanceConfigs();
|
||
$dirPath = dirPathMap($dirs);
|
||
$dirScope = [];
|
||
if ($dirId > 0) {
|
||
$dirScope = collectSubDirIds($dirs, $dirId);
|
||
}
|
||
$archives = array_values(array_filter($rows, function ($r) use ($q, $statusFilter, $dirScope, $searchField, $financeAllGlobal, $kindFilter) {
|
||
if ($dirScope && !in_array((int)$r['directory_id'], $dirScope, true)) {
|
||
return false;
|
||
}
|
||
$rowKind = normalizeArchiveKind((string)($r['archive_kind'] ?? ''));
|
||
if ($kindFilter !== 'all' && $rowKind !== $kindFilter) {
|
||
return false;
|
||
}
|
||
$status = archiveStatusWithConfig($r['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$r['id']));
|
||
if ($statusFilter === 'not_done' && $status === '已完成') {
|
||
return false;
|
||
}
|
||
if (!in_array($statusFilter, ['all', 'not_done'], true) && $statusFilter !== $status) {
|
||
return false;
|
||
}
|
||
if ($q !== '') {
|
||
if (!in_array($searchField, ['all', 'borrower', 'guarantor'], true)) {
|
||
$searchField = 'all';
|
||
}
|
||
if ($searchField === 'borrower') {
|
||
$hay = mb_strtolower((string)$r['borrower']);
|
||
} elseif ($searchField === 'guarantor') {
|
||
$hay = mb_strtolower((string)$r['guarantor']);
|
||
} else {
|
||
$hay = mb_strtolower(implode(' ', [
|
||
$r['name'],
|
||
$r['loan_institution'],
|
||
$r['borrower'],
|
||
$r['guarantee_type'],
|
||
$r['guarantor'],
|
||
]));
|
||
}
|
||
if (mb_strpos($hay, mb_strtolower($q)) === false) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}));
|
||
|
||
if (!in_array($sortBy, ['end_date', 'created_at', 'loan_institution'], true)) {
|
||
$sortBy = 'end_date';
|
||
}
|
||
usort($archives, function ($a, $b) use ($sortBy, $sortOrder, $financeAllGlobal) {
|
||
$sa = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
|
||
$sb = archiveStatusWithConfig($b['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$b['id']));
|
||
$w = ['履行中' => 0, '逾期中' => 1, '待更新' => 2, '已完成' => 3];
|
||
$sw = ($w[$sa] ?? 9) <=> ($w[$sb] ?? 9);
|
||
if ($sw !== 0) {
|
||
return $sw;
|
||
}
|
||
$av = (string)($a[$sortBy] ?? '');
|
||
$bv = (string)($b[$sortBy] ?? '');
|
||
if ($sortBy === 'loan_institution') {
|
||
$av = mb_strtolower($av);
|
||
$bv = mb_strtolower($bv);
|
||
}
|
||
$cmp = $av <=> $bv;
|
||
if ($cmp === 0) {
|
||
$cmp = ((int)$a['id']) <=> ((int)$b['id']);
|
||
}
|
||
return $sortOrder === 'asc' ? $cmp : -$cmp;
|
||
});
|
||
|
||
$totalCount = count($archives);
|
||
$activeCount = 0;
|
||
$doneCount = 0;
|
||
foreach ($archives as $item) {
|
||
$status = archiveStatusWithConfig($item['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$item['id']));
|
||
if ($status === '履行中') {
|
||
$activeCount++;
|
||
} elseif ($status === '已完成') {
|
||
$doneCount++;
|
||
} elseif ($status === '逾期中') {
|
||
$activeCount++;
|
||
}
|
||
}
|
||
|
||
$today = date('Y-m-d');
|
||
$day60 = date('Y-m-d', strtotime('+60 days'));
|
||
$dueSoonLoans = array_values(array_filter($archives, function ($a) use ($today, $day60, $financeAllGlobal) {
|
||
if (empty($a['end_date'])) {
|
||
return false;
|
||
}
|
||
$status = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
|
||
if ($status === '已完成') {
|
||
return false;
|
||
}
|
||
return $a['end_date'] >= $today && $a['end_date'] <= $day60;
|
||
}));
|
||
$dueSoonCount = count($dueSoonLoans);
|
||
$dueSoonLoans = array_slice($dueSoonLoans, 0, 8);
|
||
$overdueLoans = array_values(array_filter($archives, function ($a) use ($financeAllGlobal) {
|
||
$status = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
|
||
return $status === '逾期中';
|
||
}));
|
||
$overdueLoanCount = count($overdueLoans);
|
||
$overdueLoans = array_slice($overdueLoans, 0, 8);
|
||
$repayPlanSoon = [];
|
||
$overduePlanRows = [];
|
||
$planAllForReminder = loadRepaymentPlans();
|
||
foreach ($planAllForReminder as $aid => $cfg) {
|
||
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
|
||
continue;
|
||
}
|
||
$aidInt = (int)$aid;
|
||
$arc = null;
|
||
foreach ($archives as $tmpArc) {
|
||
if ((int)$tmpArc['id'] === $aidInt) {
|
||
$arc = $tmpArc;
|
||
break;
|
||
}
|
||
}
|
||
if (!$arc) {
|
||
continue;
|
||
}
|
||
$day30 = date('Y-m-d', strtotime('+30 days'));
|
||
foreach ($cfg['plans'] as $p) {
|
||
$status = (string)($p['status'] ?? 'pending');
|
||
$d = (string)($p['due_date'] ?? '');
|
||
if ($status === 'paid' || $d === '') {
|
||
continue;
|
||
}
|
||
if ($d >= $today && $d <= $day30) {
|
||
$repayPlanSoon[] = [
|
||
'archive_id' => $aidInt,
|
||
'plan_id' => (string)($p['id'] ?? ''),
|
||
'archive_kind' => normalizeArchiveKind((string)($arc['archive_kind'] ?? '')),
|
||
'loan_institution' => $arc['loan_institution'] ?? '',
|
||
'borrower' => $arc['borrower'] ?? '',
|
||
'amount' => (float)($p['amount'] ?? 0),
|
||
'due_date' => $d,
|
||
];
|
||
}
|
||
if ($status === 'overdue' && $d <= $today) {
|
||
$overduePlanRows[] = [
|
||
'archive_id' => $aidInt,
|
||
'plan_id' => (string)($p['id'] ?? ''),
|
||
'archive_kind' => normalizeArchiveKind((string)($arc['archive_kind'] ?? '')),
|
||
'loan_institution' => $arc['loan_institution'] ?? '',
|
||
'borrower' => $arc['borrower'] ?? '',
|
||
'amount' => (float)($p['amount'] ?? 0),
|
||
'due_date' => $d,
|
||
];
|
||
}
|
||
}
|
||
}
|
||
$repaySoonCount = count($repayPlanSoon);
|
||
$repayPlanSoon = array_slice($repayPlanSoon, 0, 8);
|
||
$overdueCount = $overdueLoanCount + count($overduePlanRows);
|
||
|
||
$pendingUpdateRows = [];
|
||
foreach ($archives as $a) {
|
||
$aid = (int)$a['id'];
|
||
$end = (string)($a['end_date'] ?? '');
|
||
if ($end === '' || $end >= $today) {
|
||
continue;
|
||
}
|
||
if (archiveStatusWithConfig($end, getArchiveFinanceConfig($financeAllGlobal, $aid)) !== '待更新') {
|
||
continue;
|
||
}
|
||
$pendingUpdateRows[] = [
|
||
'row_kind' => 'loan',
|
||
'archive_id' => $aid,
|
||
'type_label' => '贷款到期',
|
||
'loan_institution' => (string)($a['loan_institution'] ?? ''),
|
||
'borrower' => (string)($a['borrower'] ?? ''),
|
||
'guarantor' => (string)($a['guarantor'] ?? ''),
|
||
'amount' => $a['remaining_limit'] !== null ? (float)$a['remaining_limit'] : null,
|
||
'date' => $end,
|
||
'plan_id' => '',
|
||
];
|
||
}
|
||
$archiveIdsInScope = [];
|
||
foreach ($archives as $a) {
|
||
$archiveIdsInScope[(int)$a['id']] = true;
|
||
}
|
||
foreach ($planAllForReminder as $aidStr => $cfg) {
|
||
$aidInt = (int)$aidStr;
|
||
if (empty($archiveIdsInScope[$aidInt])) {
|
||
continue;
|
||
}
|
||
if (empty($cfg['enabled']) || empty($cfg['plans']) || !is_array($cfg['plans'])) {
|
||
continue;
|
||
}
|
||
$arc = null;
|
||
foreach ($archives as $tmp) {
|
||
if ((int)$tmp['id'] === $aidInt) {
|
||
$arc = $tmp;
|
||
break;
|
||
}
|
||
}
|
||
if (!$arc) {
|
||
continue;
|
||
}
|
||
foreach ($cfg['plans'] as $p) {
|
||
$pst = (string)($p['status'] ?? 'pending');
|
||
$d = (string)($p['due_date'] ?? '');
|
||
if ($d === '' || $d >= $today) {
|
||
continue;
|
||
}
|
||
if (!in_array($pst, ['pending', 'delayed'], true)) {
|
||
continue;
|
||
}
|
||
$pendingUpdateRows[] = [
|
||
'row_kind' => 'plan',
|
||
'archive_id' => $aidInt,
|
||
'type_label' => '还款计划到期',
|
||
'loan_institution' => (string)($arc['loan_institution'] ?? ''),
|
||
'borrower' => (string)($arc['borrower'] ?? ''),
|
||
'guarantor' => (string)($arc['guarantor'] ?? ''),
|
||
'amount' => (float)($p['amount'] ?? 0),
|
||
'date' => $d,
|
||
'plan_id' => (string)($p['id'] ?? ''),
|
||
];
|
||
}
|
||
}
|
||
usort($pendingUpdateRows, function ($x, $y) {
|
||
$c = strcmp((string)($x['date'] ?? ''), (string)($y['date'] ?? ''));
|
||
if ($c !== 0) {
|
||
return $c;
|
||
}
|
||
return ((int)($x['archive_id'] ?? 0)) <=> ((int)($y['archive_id'] ?? 0));
|
||
});
|
||
$pendingUpdateCount = count($pendingUpdateRows);
|
||
|
||
if ($action === 'pending_updates') {
|
||
requireLogin();
|
||
$puBack = 'index.php';
|
||
if ($dirId > 0) {
|
||
$puBack .= '?dir_id=' . $dirId;
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3 class="section-title">待更新详情</h3>
|
||
<p class="small muted">以下为已过到期日且尚未在系统中标记结果的贷款与还款计划。处理后将从本列表移除。</p>
|
||
<p style="margin:10px 0"><a class="clear-link" href="<?= e($puBack) ?>">← 返回首页</a></p>
|
||
<div class="table-wrap"><table>
|
||
<thead><tr><th>类型</th><th>贷款机构</th><th>借款主体</th><th>担保人</th><th>金额</th><th>日期</th><th>操作</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($pendingUpdateRows as $r): ?>
|
||
<tr>
|
||
<td><span class="badge <?= ($r['row_kind'] ?? '') === 'loan' ? 'b1' : 'b2' ?>"><?= e((string)($r['type_label'] ?? '')) ?></span></td>
|
||
<td><?= e((string)($r['loan_institution'] ?? '')) ?></td>
|
||
<td><?= e((string)($r['borrower'] ?? '')) ?></td>
|
||
<td><?= e((string)($r['guarantor'] ?? '')) ?></td>
|
||
<td><?= e(amount(isset($r['amount']) && $r['amount'] !== null ? (float)$r['amount'] : null)) ?></td>
|
||
<td><?= e((string)($r['date'] ?? '')) ?></td>
|
||
<td>
|
||
<a class="icon-btn" href="index.php?action=archive_detail&id=<?= (int)($r['archive_id'] ?? 0) ?>">查看档案</a>
|
||
<?php if (isAdmin()): ?>
|
||
<?php if (($r['row_kind'] ?? '') === 'loan'): ?>
|
||
<form method="post" action="index.php?action=pending_update_action" class="inline" style="display:inline" onsubmit="return confirm('确认标记为已还款?状态将改为已完成。')">
|
||
<input type="hidden" name="op" value="pending_loan_repaid">
|
||
<input type="hidden" name="archive_id" value="<?= (int)($r['archive_id'] ?? 0) ?>">
|
||
<?php if ($dirId > 0): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn-gray">已还款</button>
|
||
</form>
|
||
<form method="post" action="index.php?action=pending_update_action" class="inline" style="display:inline" onsubmit="return confirm('确认标记为已逾期?状态将改为逾期中。')">
|
||
<input type="hidden" name="op" value="pending_loan_overdue">
|
||
<input type="hidden" name="archive_id" value="<?= (int)($r['archive_id'] ?? 0) ?>">
|
||
<?php if ($dirId > 0): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn-gray">已逾期</button>
|
||
</form>
|
||
<?php else: ?>
|
||
<form method="post" action="index.php?action=pending_update_action" class="inline" style="display:inline" onsubmit="return confirm('确认该期已还款?状态将改为按期还款(已还款)。')">
|
||
<input type="hidden" name="op" value="pending_plan_repaid">
|
||
<input type="hidden" name="archive_id" value="<?= (int)($r['archive_id'] ?? 0) ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e((string)($r['plan_id'] ?? '')) ?>">
|
||
<?php if ($dirId > 0): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn-gray">已还款</button>
|
||
</form>
|
||
<form method="post" action="index.php?action=pending_update_action" class="inline" style="display:inline" onsubmit="return confirm('确认该期已逾期?状态将改为逾期中。')">
|
||
<input type="hidden" name="op" value="pending_plan_overdue">
|
||
<input type="hidden" name="archive_id" value="<?= (int)($r['archive_id'] ?? 0) ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e((string)($r['plan_id'] ?? '')) ?>">
|
||
<?php if ($dirId > 0): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn-gray">已逾期</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($pendingUpdateRows)): ?>
|
||
<tr><td colspan="7" class="muted">暂无待更新项</td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table></div>
|
||
</div>
|
||
<?php pageFooter();
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'reminders') {
|
||
requireLogin();
|
||
$reminderType = (string)($_GET['type'] ?? 'due');
|
||
$exportFormat = (string)($_GET['format'] ?? '');
|
||
$allReminderRows = [];
|
||
foreach ($dueSoonLoans as $r) {
|
||
$allReminderRows[] = ['type' => '贷款到期(60天)', 'archive_id' => (int)$r['id'], 'plan_id' => '', 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null), 'date' => (string)$r['end_date']];
|
||
}
|
||
foreach ($repayPlanSoon as $r) {
|
||
$allReminderRows[] = ['type' => '还款计划(30天)', 'archive_id' => (int)$r['archive_id'], 'plan_id' => (string)$r['plan_id'], 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount((float)$r['amount']), 'date' => (string)$r['due_date']];
|
||
}
|
||
foreach ($overdueLoans as $r) {
|
||
$allReminderRows[] = ['type' => '逾期中', 'archive_id' => (int)$r['id'], 'plan_id' => '', 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount($r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null), 'date' => (string)$r['end_date']];
|
||
}
|
||
foreach ($overduePlanRows as $r) {
|
||
$allReminderRows[] = ['type' => '逾期中', 'archive_id' => (int)$r['archive_id'], 'plan_id' => (string)$r['plan_id'], 'archive_kind' => archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))), 'loan_institution' => (string)$r['loan_institution'], 'borrower' => (string)$r['borrower'], 'amount' => amount((float)$r['amount']), 'date' => (string)$r['due_date']];
|
||
}
|
||
$rows = array_values(array_filter($allReminderRows, function ($row) use ($reminderType) {
|
||
if ($reminderType === 'all') return true;
|
||
if ($reminderType === 'due') return $row['type'] === '贷款到期(60天)';
|
||
if ($reminderType === 'plan') return $row['type'] === '还款计划(30天)';
|
||
if ($reminderType === 'overdue') return $row['type'] === '逾期中';
|
||
return true;
|
||
}));
|
||
if ($exportFormat !== '') {
|
||
$headers = ['类型', '档案类型', '贷款机构', '借款主体', '金额', '日期'];
|
||
if ($exportFormat === 'xlsx' && class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet') && class_exists('\PhpOffice\PhpSpreadsheet\Writer\Xlsx')) {
|
||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$sheet->fromArray($headers, null, 'A1');
|
||
$sheet->fromArray(array_map(fn($x) => [$x['type'], $x['archive_kind'], $x['loan_institution'], $x['borrower'], $x['amount'], $x['date']], $rows), null, 'A2');
|
||
foreach (range('A', 'F') as $col) { $sheet->getColumnDimension($col)->setWidth(20); }
|
||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode('提醒明细_' . date('Y-m-d') . '.xlsx') . '"');
|
||
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet))->save('php://output');
|
||
exit;
|
||
}
|
||
header('Content-Type: text/csv; charset=UTF-8');
|
||
header('Content-Disposition: attachment; filename="' . rawurlencode('提醒明细_' . date('Y-m-d') . '.csv') . '"');
|
||
echo "\xEF\xBB\xBF";
|
||
$out = fopen('php://output', 'wb');
|
||
fputcsv($out, $headers);
|
||
foreach ($rows as $r) { fputcsv($out, [$r['type'], $r['archive_kind'], $r['loan_institution'], $r['borrower'], $r['amount'], $r['date']]); }
|
||
fclose($out);
|
||
exit;
|
||
}
|
||
pageHeader($me); ?>
|
||
<div class="card">
|
||
<h3 class="section-title">到期提醒详情</h3>
|
||
<form method="get" class="toolbar-left" style="margin-bottom:10px">
|
||
<input type="hidden" name="action" value="reminders">
|
||
<select name="type">
|
||
<option value="all" <?= $reminderType === 'all' ? 'selected' : '' ?>>全部类型</option>
|
||
<option value="due" <?= $reminderType === 'due' ? 'selected' : '' ?>>贷款到期</option>
|
||
<option value="plan" <?= $reminderType === 'plan' ? 'selected' : '' ?>>还款计划</option>
|
||
<option value="overdue" <?= $reminderType === 'overdue' ? 'selected' : '' ?>>贷款逾期</option>
|
||
</select>
|
||
<button type="submit">筛选</button>
|
||
<a class="clear-link" href="index.php?action=reminders&type=due">清空</a>
|
||
<button type="submit" name="format" value="xlsx" class="toolbar-btn">导出xlsx</button>
|
||
<button type="submit" name="format" value="csv" class="toolbar-btn">导出csv</button>
|
||
</form>
|
||
<div class="table-wrap"><table>
|
||
<thead><tr><th>类型</th><th>档案类型</th><th>贷款机构</th><th>借款主体</th><th>金额</th><th>日期</th><th>操作</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($rows as $r): ?>
|
||
<tr>
|
||
<td><span class="badge <?= $r['type'] === '贷款到期(60天)' ? 'b1' : ($r['type'] === '还款计划(30天)' ? 'b2' : 'b4') ?>"><?= e($r['type']) ?></span></td>
|
||
<td><?= e((string)($r['archive_kind'] ?? '')) ?></td>
|
||
<td><?= e($r['loan_institution']) ?></td>
|
||
<td><?= e($r['borrower']) ?></td>
|
||
<td><?= e($r['amount']) ?></td>
|
||
<td><?= e($r['date']) ?></td>
|
||
<td>
|
||
<?php if ((int)($r['archive_id'] ?? 0) > 0): ?>
|
||
<a class="icon-btn" href="index.php?action=archive_detail&id=<?= (int)$r['archive_id'] ?>">查看档案</a>
|
||
<?php endif; ?>
|
||
<?php if ((int)($r['archive_id'] ?? 0) > 0 && $r['type'] === '贷款到期(60天)' && isAdmin()): ?>
|
||
<form method="post" action="index.php?action=reminder_action" class="inline" onsubmit="return confirm('确认设置该档案为提前还款并标记已完成?')">
|
||
<input type="hidden" name="op" value="early_settle">
|
||
<input type="hidden" name="archive_id" value="<?= (int)$r['archive_id'] ?>">
|
||
<input type="hidden" name="redirect_type" value="<?= e($reminderType) ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn-gray">提前还款</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php if ((int)($r['archive_id'] ?? 0) > 0 && $r['type'] === '还款计划(30天)' && isAdmin()): ?>
|
||
<form method="post" action="index.php?action=reminder_action" class="inline">
|
||
<input type="hidden" name="op" value="plan_delay">
|
||
<input type="hidden" name="archive_id" value="<?= (int)$r['archive_id'] ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e((string)($r['plan_id'] ?? '')) ?>">
|
||
<input type="hidden" name="redirect_type" value="<?= e($reminderType) ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<input type="date" name="new_due_date" required style="max-width:140px">
|
||
<button type="submit" class="btn-gray">延期</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php if ((int)($r['archive_id'] ?? 0) > 0 && $r['type'] === '逾期中' && isAdmin()): ?>
|
||
<form method="post" action="index.php?action=reminder_action" class="inline" onsubmit="return confirm('确认将该逾期项标记为已归还?')">
|
||
<input type="hidden" name="op" value="overdue_paid">
|
||
<input type="hidden" name="archive_id" value="<?= (int)$r['archive_id'] ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e((string)($r['plan_id'] ?? '')) ?>">
|
||
<input type="hidden" name="redirect_type" value="<?= e($reminderType) ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn-gray">已归还</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php if ((int)($r['archive_id'] ?? 0) <= 0): ?>
|
||
<span class="muted">-</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($rows)): ?>
|
||
<tr><td colspan="7" class="muted">暂无提醒内容</td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table></div>
|
||
</div>
|
||
<?php pageFooter(); exit;
|
||
}
|
||
|
||
$perPage = (int)($_GET['per_page'] ?? 10);
|
||
if (!in_array($perPage, [10, 15, 30, 50], true)) {
|
||
$perPage = 10;
|
||
}
|
||
$currentPage = max(1, (int)($_GET['page'] ?? 1));
|
||
$totalPages = max(1, (int)ceil($totalCount / $perPage));
|
||
if ($currentPage > $totalPages) {
|
||
$currentPage = $totalPages;
|
||
}
|
||
$offset = ($currentPage - 1) * $perPage;
|
||
$archivesPage = array_slice($archives, $offset, $perPage);
|
||
|
||
$baseQuery = ['q' => $q, 'search_field' => $searchField, 'status' => $statusFilter, 'kind' => $kindFilter, 'per_page' => $perPage, 'sort_by' => $sortBy, 'sort_order' => $sortOrder];
|
||
if ($dirId) {
|
||
$baseQuery['dir_id'] = $dirId;
|
||
}
|
||
unset($baseQuery['page']);
|
||
$pathIds = collectAncestorIds($dirs, $dirId ?: null);
|
||
|
||
pageHeader($me);
|
||
?>
|
||
<div class="grid">
|
||
<div class="card tree">
|
||
<div class="toolbar" style="margin-bottom:8px">
|
||
<h3 class="section-title" style="margin:0">目录结构</h3>
|
||
<?php if (isAdmin()): ?><a class="toolbar-btn" href="index.php?action=new_dir">+ 一级目录</a><?php endif; ?>
|
||
</div>
|
||
<?= buildTreeHtml($dirs, $dirId ?: null, isAdmin(), $pathIds) ?>
|
||
<div style="margin-top:12px;border-top:1px solid var(--line);padding-top:10px">
|
||
<h3 class="section-title" style="margin:0 0 8px 0">便捷查询</h3>
|
||
<?= quickQueryMenuHtml() ?>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="metric-wrap">
|
||
<div class="metric m-all"><div class="k">当前结果总数</div><div class="v"><?= $totalCount ?></div></div>
|
||
<div class="metric m-active"><div class="k">履行中</div><div class="v"><?= $activeCount ?></div></div>
|
||
<div class="metric m-done"><div class="k">已完成</div><div class="v"><?= $doneCount ?></div></div>
|
||
<a class="metric m-unset subtle-link" href="index.php?action=pending_updates<?= $dirId ? '&dir_id=' . $dirId : '' ?>"><div class="k">待更新</div><div class="v"><?= $pendingUpdateCount ?></div></a>
|
||
<a class="metric m-due subtle-link" href="index.php?action=reminders&type=due"><div class="k">贷款到期</div><div class="v"><?= $dueSoonCount ?></div></a>
|
||
<a class="metric m-plan subtle-link" href="index.php?action=reminders&type=plan"><div class="k">还款计划</div><div class="v"><?= $repaySoonCount ?></div></a>
|
||
<a class="metric m-overdue subtle-link" href="index.php?action=reminders&type=overdue"><div class="k">贷款逾期</div><div class="v"><?= $overdueCount ?></div></a>
|
||
</div>
|
||
<div class="card" style="margin-top:12px">
|
||
<div class="toolbar">
|
||
<h3 class="section-title" style="margin:0">档案列表</h3>
|
||
<div class="toolbar-right">
|
||
<?php if ($dirId): ?><span class="pill">当前目录筛选中</span><?php endif; ?>
|
||
<?php if (isAdmin()): ?>
|
||
<a class="toolbar-btn primary" href="index.php?action=archive_form<?= $dirId ? '&dir_id=' . $dirId : '' ?>">+ 新建档案</a>
|
||
<?php endif; ?>
|
||
<?php if (isAdmin()): ?>
|
||
<form method="post" action="index.php?action=import_archives" enctype="multipart/form-data" class="inline">
|
||
<input type="file" name="import_file" required>
|
||
<button type="submit" class="toolbar-btn">导入模板</button>
|
||
</form>
|
||
<a class="toolbar-btn" href="index.php?action=download_import_template">模板下载</a>
|
||
<?php endif; ?>
|
||
<form method="get" action="index.php" class="inline">
|
||
<input type="hidden" name="action" value="export">
|
||
<input type="hidden" name="mode" value="all">
|
||
<?php if ($dirId): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<input type="hidden" name="kind" value="<?= e($kindFilter) ?>">
|
||
<select name="format">
|
||
<option value="xlsx">xlsx</option>
|
||
<option value="csv">csv</option>
|
||
</select>
|
||
<button type="submit" class="toolbar-btn">⭳ 导出全部</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<form method="get" style="margin-bottom:10px">
|
||
<div class="toolbar-left">
|
||
<?php if ($dirId): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<select name="kind">
|
||
<option value="<?= e(ARCHIVE_KIND_BANK_LOAN) ?>" <?= $kindFilter === ARCHIVE_KIND_BANK_LOAN ? 'selected' : '' ?>>仅银行贷款</option>
|
||
<option value="<?= e(ARCHIVE_KIND_EXTERNAL_GUARANTEE) ?>" <?= $kindFilter === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? 'selected' : '' ?>>仅对外担保</option>
|
||
<option value="all" <?= $kindFilter === 'all' ? 'selected' : '' ?>>全部类型</option>
|
||
</select>
|
||
<select name="search_field">
|
||
<option value="all" <?= $searchField === 'all' ? 'selected' : '' ?>>全部</option>
|
||
<option value="borrower" <?= $searchField === 'borrower' ? 'selected' : '' ?>>借款主体</option>
|
||
<option value="guarantor" <?= $searchField === 'guarantor' ? 'selected' : '' ?>>担保人</option>
|
||
</select>
|
||
<input name="q" placeholder="输入关键词后回车或点筛选" value="<?= e($q) ?>">
|
||
<select name="status">
|
||
<option value="all" <?= $statusFilter === 'all' ? 'selected' : '' ?>>全部状态</option>
|
||
<option value="not_done" <?= $statusFilter === 'not_done' ? 'selected' : '' ?>>默认(隐藏已完成)</option>
|
||
<option value="履行中" <?= $statusFilter === '履行中' ? 'selected' : '' ?>>履行中</option>
|
||
<option value="逾期中" <?= $statusFilter === '逾期中' ? 'selected' : '' ?>>逾期中</option>
|
||
<option value="已完成" <?= $statusFilter === '已完成' ? 'selected' : '' ?>>已完成</option>
|
||
<option value="待更新" <?= $statusFilter === '待更新' ? 'selected' : '' ?>>待更新</option>
|
||
</select>
|
||
<select name="sort_by">
|
||
<option value="created_at" <?= $sortBy === 'created_at' ? 'selected' : '' ?>>按创建时间</option>
|
||
<option value="end_date" <?= $sortBy === 'end_date' ? 'selected' : '' ?>>按到期日</option>
|
||
<option value="loan_institution" <?= $sortBy === 'loan_institution' ? 'selected' : '' ?>>按贷款机构</option>
|
||
</select>
|
||
<select name="sort_order">
|
||
<option value="desc" <?= $sortOrder === 'desc' ? 'selected' : '' ?>>倒序</option>
|
||
<option value="asc" <?= $sortOrder === 'asc' ? 'selected' : '' ?>>正序</option>
|
||
</select>
|
||
<select name="per_page">
|
||
<option value="10" <?= $perPage === 10 ? 'selected' : '' ?>>每页10条</option>
|
||
<option value="15" <?= $perPage === 15 ? 'selected' : '' ?>>每页15条</option>
|
||
<option value="30" <?= $perPage === 30 ? 'selected' : '' ?>>每页30条</option>
|
||
<option value="50" <?= $perPage === 50 ? 'selected' : '' ?>>每页50条</option>
|
||
</select>
|
||
<button type="submit">筛选</button>
|
||
<a class="clear-link" href="index.php<?= $dirId ? '?dir_id=' . $dirId : '' ?>">一键清空</a>
|
||
</div>
|
||
</form>
|
||
<form method="get" action="index.php">
|
||
<input type="hidden" name="action" value="export">
|
||
<input type="hidden" name="mode" value="selected">
|
||
<input type="hidden" name="kind" value="<?= e($kindFilter) ?>">
|
||
<select name="format">
|
||
<option value="xlsx">xlsx</option>
|
||
<option value="csv">csv</option>
|
||
</select>
|
||
<?php if ($dirId): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<div class="table-wrap"><table class="fixed-table archive-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="col-check"><input type="checkbox" onclick="document.querySelectorAll('.cid').forEach(x=>x.checked=this.checked)"></th>
|
||
<th class="col-kind">类型</th>
|
||
<th class="col-name">档案名称</th>
|
||
<th class="col-inst"><span class="th-stack"><span>机构</span><span class="th-sub">贷款行/债权人</span></span></th>
|
||
<th class="col-borrower">借款主体</th>
|
||
<th class="col-amount hide-md">授信额度</th>
|
||
<th class="col-amount">剩余额度</th>
|
||
<th class="col-date hide-md">起始日</th>
|
||
<th class="col-date">到期日</th>
|
||
<th class="col-guarantor">担保人</th>
|
||
<th class="col-rate">年利率</th>
|
||
<th class="col-status">状态</th>
|
||
<th class="col-action">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($archivesPage as $a):
|
||
$s = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
|
||
$rowKind = normalizeArchiveKind($a['archive_kind'] ?? '');
|
||
$isExtGuarantee = $rowKind === ARCHIVE_KIND_EXTERNAL_GUARANTEE;
|
||
?>
|
||
<tr data-archive-kind="<?= e($rowKind) ?>">
|
||
<td class="col-check" data-label="选择"><input class="cid" type="checkbox" name="ids[]" value="<?= (int)$a['id'] ?>"></td>
|
||
<td class="col-kind" data-label="类型"><?php if ($isExtGuarantee): ?><span class="badge-k-guar">担保</span><?php else: ?><span class="badge-k-loan">贷款</span><?php endif; ?></td>
|
||
<td class="col-name" data-label="档案名称"><span class="truncate" title="<?= e((string)$a['name']) ?>"><?= e((string)$a['name']) ?></span></td>
|
||
<td class="col-inst" data-label="贷款机构"><span class="truncate" title="<?= e((string)$a['loan_institution']) ?>"><?= e((string)$a['loan_institution']) ?></span></td>
|
||
<td class="col-borrower" data-label="借款主体"><span class="truncate" title="<?= e((string)$a['borrower']) ?>"><?= e((string)$a['borrower']) ?></span></td>
|
||
<td class="hide-md" data-label="授信额度"><?= e(amount($a['credit_limit'] !== null ? (float)$a['credit_limit'] : null)) ?></td>
|
||
<td data-label="剩余额度"><?= e(amount($a['remaining_limit'] !== null ? (float)$a['remaining_limit'] : null)) ?></td>
|
||
<td class="col-date hide-md" data-label="起始日"><span class="truncate" title="<?= e((string)$a['start_date']) ?>"><?= e((string)$a['start_date']) ?></span></td>
|
||
<td class="col-date" data-label="到期日"><span class="truncate" title="<?= e((string)$a['end_date']) ?>"><?= e((string)$a['end_date']) ?></span></td>
|
||
<td class="col-guarantor" data-label="担保人"><span class="truncate" title="<?= e((string)$a['guarantor']) ?>"><?= e((string)$a['guarantor']) ?></span></td>
|
||
<td class="col-rate" data-label="年利率"><?= e($a['annual_rate'] === null ? '' : ((string)$a['annual_rate'] . '%')) ?></td>
|
||
<td class="col-status" data-label="状态">
|
||
<span class="badge <?= $s === '履行中' ? 'b1' : ($s === '已完成' ? 'b2' : ($s === '逾期中' ? 'b4' : 'b3')) ?>"><?= e($s) ?></span>
|
||
</td>
|
||
<td class="col-action" data-label="操作">
|
||
<div class="icon-actions">
|
||
<a class="icon-btn" href="index.php?action=archive_detail&id=<?= (int)$a['id'] ?>">👁 查看</a>
|
||
<?php if (isAdmin()): ?>
|
||
<a class="icon-btn" href="index.php?action=archive_form&id=<?= (int)$a['id'] ?>">✎ 编辑</a>
|
||
<a class="icon-btn danger" href="index.php?action=request_delete_archive&id=<?= (int)$a['id'] ?>" onclick="return confirm('将提交双管理员删除申请,确认?')">🗑 删除</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table></div>
|
||
<p style="display:flex;gap:8px;flex-wrap:wrap">
|
||
<button type="submit">导出选中档案</button>
|
||
<?php if (isAdmin()): ?>
|
||
<button type="button" class="btn-danger" onclick="requestDeleteSelected()">删除选中档案(双管理员)</button>
|
||
<?php endif; ?>
|
||
</p>
|
||
</form>
|
||
<script>
|
||
function requestDeleteSelected(){
|
||
var ids=[...document.querySelectorAll('.cid:checked')].map(function(x){return x.value;});
|
||
if(ids.length===0){alert('请先选择要删除的档案');return;}
|
||
if(!confirm('将提交双管理员删除申请,确认?')){return;}
|
||
window.location.href='index.php?action=request_delete_selected&ids='+encodeURIComponent(ids.join(','));
|
||
}
|
||
</script>
|
||
<div class="pager">
|
||
<div class="small">第 <?= $currentPage ?> / <?= $totalPages ?> 页,共 <?= $totalCount ?> 条</div>
|
||
<div>
|
||
<?php
|
||
$prevQuery = http_build_query(array_merge($baseQuery, ['page' => max(1, $currentPage - 1)]));
|
||
$nextQuery = http_build_query(array_merge($baseQuery, ['page' => min($totalPages, $currentPage + 1)]));
|
||
?>
|
||
<?php if ($currentPage > 1): ?><a href="index.php?<?= $prevQuery ?>">上一页</a><?php else: ?><span class="muted">上一页</span><?php endif; ?>
|
||
<span class="muted"> | </span>
|
||
<?php if ($currentPage < $totalPages): ?><a href="index.php?<?= $nextQuery ?>">下一页</a><?php else: ?><span class="muted">下一页</span><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
pageFooter();
|