2989 lines
135 KiB
PHP
Executable File
2989 lines
135 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 (portalEnabled() && in_array($action, ['login','logout','profile','users'], true)) {
|
||
portalFail(403, '账号和登录请前往统一门户管理。');
|
||
}
|
||
|
||
if ($action === 'logout') {
|
||
session_destroy();
|
||
header('Location: index.php?action=login');
|
||
exit;
|
||
}
|
||
|
||
if ($action === 'login') { require __DIR__ . '/../app/views/pages/login.php'; exit; }
|
||
|
||
requireLogin();
|
||
$me = currentUser();
|
||
if ($action === 'home') {
|
||
$qs = (string)($_SERVER['QUERY_STRING'] ?? '');
|
||
$_SESSION['last_list_url'] = 'index.php' . ($qs !== '' ? ('?' . $qs) : '');
|
||
}
|
||
|
||
/* ---------- 视觉层辅助(仅呈现,不改业务语义) ---------- */
|
||
if (!function_exists('fmtMoney')) {
|
||
function fmtMoney(?float $v): string
|
||
{
|
||
return $v === null ? '—' : number_format($v, 2, '.', ',');
|
||
}
|
||
}
|
||
if (!function_exists('fmtWan')) {
|
||
function fmtWan(?float $v): string
|
||
{
|
||
if ($v === null) {
|
||
return '—';
|
||
}
|
||
if (abs($v) >= 10000) {
|
||
$s = number_format($v / 10000, 2, '.', ',');
|
||
$s = rtrim(rtrim($s, '0'), '.');
|
||
return $s . ' 万';
|
||
}
|
||
return number_format($v, 2, '.', ',');
|
||
}
|
||
}
|
||
if (!function_exists('statusPillCls')) {
|
||
function statusPillCls(string $s): string
|
||
{
|
||
return ['履行中' => 'ok', '逾期中' => 'over', '待更新' => 'pending', '已完成' => 'done'][$s] ?? 'pending';
|
||
}
|
||
}
|
||
|
||
function navActiveKey(string $action): string
|
||
{
|
||
$map = [
|
||
'workbench' => 'workbench',
|
||
'home' => 'list',
|
||
'archive_detail' => 'list',
|
||
'archive_form' => 'list',
|
||
'new_dir' => 'list',
|
||
'edit_dir' => 'list',
|
||
'tasks' => 'tasks',
|
||
'reminders' => 'tasks',
|
||
'pending_updates' => 'tasks',
|
||
'query_totals' => 'query',
|
||
'query_payable_interest' => 'query',
|
||
'query_external_guarantee' => 'query',
|
||
'repayment_history' => 'query',
|
||
'quick_queries' => 'query',
|
||
'interest_calculator' => 'tools',
|
||
'acceptance_discount_calculator' => 'tools',
|
||
'delete_approvals' => 'approvals',
|
||
'users' => 'users',
|
||
'profile' => 'profile',
|
||
];
|
||
return $map[$action] ?? 'list';
|
||
}
|
||
|
||
function pageHeader(array $me, ?int $taskCount = null): void
|
||
{
|
||
global $action;
|
||
$flash = getFlash();
|
||
$navKey = navActiveKey((string)$action);
|
||
$isAdmin = ($me['role'] ?? '') === 'admin';
|
||
$who = maskUsername((string)$me['username']);
|
||
$roleTxt = $isAdmin ? '管理员' : '经办';
|
||
$avatar = $isAdmin ? '管' : '办';
|
||
$navItem = function (string $key, string $href, string $label, string $short, ?int $badge = null) use ($navKey): string {
|
||
$active = $navKey === $key ? ' data-active' : '';
|
||
$b = $badge !== null && $badge > 0 ? '<span class="nav-badge num">' . $badge . '</span>' : '';
|
||
return '<a class="nav-item" href="' . e($href) . '" data-short="' . e($short) . '"' . $active . '><span class="dot"></span>' . e($label) . $b . '</a>';
|
||
};
|
||
?>
|
||
<!doctype html>
|
||
<html lang="zh-CN"><head><meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>金牛集团贷款档案管理系统</title>
|
||
<link rel="stylesheet" href="<?= assetUrl('assets/fusion.css') ?>">
|
||
<link rel="stylesheet" href="<?= assetUrl('assets/portal-return.css') ?>">
|
||
<script src="<?= assetUrl('assets/fusion.js') ?>" defer></script>
|
||
</head><body>
|
||
<div class="v-fusion" data-variant-root>
|
||
<script>(function(){try{if(localStorage.getItem('jn_theme')==='night'){document.currentScript.parentElement.setAttribute('data-theme','night');}}catch(e){}})();</script>
|
||
<aside class="sidebar">
|
||
<div class="brand">
|
||
<img src="assets/logo-day.png" alt="金牛实业 OA 系统图标" data-brand-logo data-logo-day="assets/logo-day.png" data-logo-night="assets/logo-night.png">
|
||
<div><div class="brand-name">金牛贷款档案</div><div class="brand-sub">JINNIU ARCHIVE</div></div>
|
||
</div>
|
||
<div class="nav-label">档案</div>
|
||
<?= $navItem('workbench', 'index.php?action=workbench', '工作台', '台') ?>
|
||
<?= $navItem('list', 'index.php', '档案库', '库') ?>
|
||
<?= $navItem('tasks', 'index.php?action=tasks', '任务中心', '任', $taskCount) ?>
|
||
<div class="nav-label">分析</div>
|
||
<?= $navItem('query', 'index.php?action=query_totals', '统计查询', '查') ?>
|
||
<?= $navItem('tools', 'index.php?action=interest_calculator', '计算工具', '算') ?>
|
||
<?php if ($isAdmin): ?>
|
||
<div class="nav-label">管理</div>
|
||
<?= $navItem('approvals', 'index.php?action=delete_approvals', '删除审批', '审') ?>
|
||
<?php if (!portalEnabled()): ?>
|
||
<?= $navItem('users', 'index.php?action=users', '用户管理', '用') ?>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
<?php if (!portalEnabled()): ?>
|
||
<div class="nav-label">账户</div>
|
||
<?= $navItem('profile', 'index.php?action=profile', '个人设置', '我') ?>
|
||
<?php endif; ?>
|
||
<div class="sidebar-account">
|
||
<div class="theme-toggle" role="group" aria-label="日间 / 夜间主题切换">
|
||
<button type="button" data-theme-btn="day" aria-pressed="true">☀ 日间</button>
|
||
<button type="button" data-theme-btn="night" aria-pressed="false">☾ 夜间</button>
|
||
</div>
|
||
<?php if (!portalEnabled()): ?>
|
||
<div class="account-box">
|
||
<div class="account-id">
|
||
<div class="account-meta">
|
||
<div class="account-name"><?= e($who) ?><span class="num">(<?= e($roleTxt) ?>)</span></div>
|
||
</div>
|
||
<a class="icon-btn" href="index.php?action=logout" aria-label="退出登录" title="退出登录" onclick="return confirm('确认退出登录?')">
|
||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2H3.5A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14H6"/><path d="M10.5 5l3 3-3 3"/><path d="M13.5 8H6.5"/></svg>
|
||
</a>
|
||
</div>
|
||
<div class="account-note">金牛集团 · 内部档案系统</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</aside>
|
||
<div class="content-shell<?= portalEnabled() ? ' has-portal-bar' : '' ?>">
|
||
<?php if (portalEnabled()): ?>
|
||
<header class="loan-portal-header jn-portal-bar"><span>贷款档案管理</span><a class="jn-portal-return" data-return-portal href="<?= e(portalPublic()) ?>" title="返回统一门户" aria-label="返回统一门户"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1Z"/></svg><span>统一门户</span></a></header>
|
||
<?php endif; ?>
|
||
<main class="content">
|
||
<?php if ($flash): ?>
|
||
<?php if ($flash['type'] === 'ok'): ?>
|
||
<script>window.addEventListener('load',function(){if(window.jnToast)window.jnToast(<?= json_encode((string)$flash['msg'], JSON_UNESCAPED_UNICODE) ?>);});</script>
|
||
<?php else: ?>
|
||
<div class="flash-banner flash-banner--err" role="alert"><?= e($flash['msg']) ?></div>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
<?php
|
||
}
|
||
|
||
function pageFooter(bool $withDrawer = true, ?string $extraModalHtml = null): void
|
||
{
|
||
$isAdmin = isAdmin();
|
||
?>
|
||
</main>
|
||
</div>
|
||
<?php if ($withDrawer): ?>
|
||
<div class="scrim" data-close-drawer></div>
|
||
<aside class="drawer" role="dialog" aria-modal="true" aria-label="档案详情" data-drawer></aside>
|
||
<?php endif; ?>
|
||
<div class="modal-scrim" data-close-modal></div>
|
||
<?php if ($isAdmin): ?>
|
||
<div class="modal-card" data-modal="import" role="dialog" aria-modal="true" aria-label="导入档案">
|
||
<form method="post" action="index.php?action=import_archives" enctype="multipart/form-data">
|
||
<div class="modal-head">
|
||
<div class="modal-title">导入档案</div>
|
||
<button type="button" class="drawer-close" data-close-modal aria-label="关闭">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="section-title" style="margin-top:0;">第一步 · 下载模板</div>
|
||
<div style="display:flex; gap:10px;">
|
||
<a class="btn btn--plain" href="index.php?action=download_import_template">xlsx 模板</a>
|
||
</div>
|
||
<div class="section-title">第二步 · 上传文件</div>
|
||
<input type="file" name="import_file" accept=".xlsx,.xls,.csv" required style="width:100%;">
|
||
<div class="field-hint" style="margin-top:6px;">xlsx / csv · 单文件 ≤ 10MB</div>
|
||
<div class="section-title">校验规则</div>
|
||
<div class="calc-formula">
|
||
必填列:借款人 / 机构 / 类型 / 授信额度 / 起止日期<br>
|
||
重复档案编号自动跳过;错误行生成报告,不中断整批导入
|
||
</div>
|
||
</div>
|
||
<div class="modal-foot">
|
||
<button type="button" class="btn btn--plain" data-close-modal>取消</button>
|
||
<button type="submit" class="btn btn--primary">开始导入</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?= $extraModalHtml ?? '' ?>
|
||
</div>
|
||
</body></html>
|
||
<?php
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
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') { require __DIR__ . '/../app/views/pages/new_dir.php'; exit; }
|
||
|
||
if ($action === 'edit_dir') { require __DIR__ . '/../app/views/pages/edit_dir.php'; 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') { require __DIR__ . '/../app/views/pages/delete_approvals.php'; 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') { require __DIR__ . '/../app/views/pages/archive_detail.php'; exit; }
|
||
|
||
if ($action === 'archive_form') { require __DIR__ . '/../app/views/pages/archive_form.php'; exit; }
|
||
|
||
if ($action === 'users') { require __DIR__ . '/../app/views/pages/users.php'; exit; }
|
||
|
||
if ($action === 'profile') { require __DIR__ . '/../app/views/pages/profile.php'; exit; }
|
||
|
||
if ($action === 'quick_queries') { require __DIR__ . '/../app/views/pages/quick_queries.php'; exit; }
|
||
|
||
if ($action === 'repayment_history') { require __DIR__ . '/../app/views/pages/repayment_history.php'; 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') { require __DIR__ . '/../app/views/pages/query_totals.php'; exit; }
|
||
|
||
if ($action === 'query_payable_interest') { require __DIR__ . '/../app/views/pages/query_payable_interest.php'; exit; }
|
||
|
||
if ($action === 'query_external_guarantee') { require __DIR__ . '/../app/views/pages/query_external_guarantee.php'; 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 = normalizeSearchField($_GET['search_field'] ?? 'all');
|
||
$statusFilter = $_GET['status'] ?? 'not_done';
|
||
$kindFilter = (string)($_GET['kind'] ?? 'all');
|
||
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 (!archiveMatchesKeyword($r, $q, $searchField)) {
|
||
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);
|
||
$dueSoonLoansFull = $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);
|
||
$overdueLoansFull = $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);
|
||
$repayPlanSoonFull = $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();
|
||
// 待更新已并入任务中心(样板 IA),保留旧链接跳转
|
||
header('Location: index.php?action=tasks&tab=pending');
|
||
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;
|
||
}
|
||
// 提醒列表已并入任务中心(样板 IA),旧链接按类型跳转;导出仍由上方分支处理
|
||
$tabMap = ['due' => 'due', 'plan' => 'plan', 'overdue' => 'over'];
|
||
header('Location: index.php?action=tasks&tab=' . ($tabMap[$reminderType] ?? 'all'));
|
||
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);
|
||
$dirPickerItems = directoryPickerItems($dirs);
|
||
$dirPickerLabel = $dirId > 0 ? (string)($dirPath[$dirId] ?? '已选目录') : '全部目录';
|
||
$searchPlaceholder = [
|
||
'borrower' => '搜索借款人',
|
||
'guarantor' => '搜索担保人',
|
||
][$searchField] ?? '搜索借款人 / 名称 / 机构';
|
||
|
||
$taskTotal = $pendingUpdateCount + $dueSoonCount + $repaySoonCount + $overdueCount;
|
||
$listUrl = function (array $ov = []) use ($baseQuery): string {
|
||
$q = array_merge($baseQuery, $ov);
|
||
foreach ($q as $k => $v) {
|
||
if ($v === '' || $v === null) {
|
||
unset($q[$k]);
|
||
}
|
||
}
|
||
return 'index.php' . ($q ? ('?' . http_build_query($q)) : '');
|
||
};
|
||
|
||
/* ============================================================
|
||
工作台(样板 workbenchPage:四张风险入口卡 + 今日待办 + 快捷入口)
|
||
============================================================ */
|
||
if ($action === 'workbench') {
|
||
requireLogin();
|
||
$isAdmin = isAdmin();
|
||
$todos = [];
|
||
foreach (array_slice($pendingUpdateRows, 0, 2) as $r) {
|
||
$todos[] = ['date' => (string)$r['date'], 'what' => '结果待确认 · ' . (string)$r['type_label'], 'ref' => '#' . (int)$r['archive_id'] . ' · ' . (string)$r['borrower'], 'tab' => 'pending', 'pill' => 'pending', 'pillTxt' => '待更新'];
|
||
}
|
||
foreach (array_slice($repayPlanSoon, 0, 2) as $r) {
|
||
$todos[] = ['date' => (string)$r['due_date'], 'what' => '还款计划到期 · ' . fmtMoney((float)$r['amount']) . ' 元', 'ref' => '#' . (int)$r['archive_id'] . ' · ' . (string)$r['borrower'], 'tab' => 'plan', 'pill' => 'due', 'pillTxt' => '未还'];
|
||
}
|
||
foreach (array_slice($dueSoonLoans, 0, 2) as $r) {
|
||
$todos[] = ['date' => (string)$r['end_date'], 'what' => (string)$r['end_date'] . ' 到期 · 需安排资金', 'ref' => '#' . (int)$r['id'] . ' · ' . (string)$r['borrower'], 'tab' => 'due', 'pill' => 'due', 'pillTxt' => '即将到期'];
|
||
}
|
||
foreach (array_slice($overdueLoans, 0, 1) as $r) {
|
||
$todos[] = ['date' => (string)$r['end_date'], 'what' => '逾期跟进 · 应于 ' . (string)$r['end_date'], 'ref' => '#' . (int)$r['id'] . ' · ' . (string)$r['borrower'], 'tab' => 'over', 'pill' => 'over', 'pillTxt' => '逾期'];
|
||
}
|
||
foreach (array_slice($overduePlanRows, 0, 1) as $r) {
|
||
$todos[] = ['date' => (string)$r['due_date'], 'what' => '逾期计划跟进 · 应于 ' . (string)$r['due_date'], 'ref' => '#' . (int)$r['archive_id'] . ' · ' . (string)$r['borrower'], 'tab' => 'over', 'pill' => 'over', 'pillTxt' => '逾期'];
|
||
}
|
||
$weekMap = ['日', '一', '二', '三', '四', '五', '六'];
|
||
pageHeader($me, $taskTotal);
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1 class="page-title">工作台</h1>
|
||
<div class="page-sub">每日巡检 · <?= e($today) ?> 星期<?= $weekMap[(int)date('w')] ?></div>
|
||
</div>
|
||
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
|
||
<?php if ($isAdmin): ?><button type="button" class="btn btn--plain" data-open-modal="import">导入</button><?php endif; ?>
|
||
<a class="btn btn--plain" href="<?= e($listUrl(['action' => 'export', 'mode' => 'all', 'format' => 'xlsx'])) ?>">导出</a>
|
||
<?php if ($isAdmin): ?><a class="btn btn--primary" href="index.php?action=archive_form<?= $dirId ? '&dir_id=' . $dirId : '' ?>">+ 新建档案</a><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="stats">
|
||
<a class="stat stat--link stagger-in" href="index.php?action=tasks&tab=pending">
|
||
<div class="stat-label">待更新</div>
|
||
<div class="stat-value num"><?= $pendingUpdateCount ?><small>笔</small></div>
|
||
<div class="stat-foot">已到结息日,待经办确认 →</div>
|
||
</a>
|
||
<a class="stat stat--link stat--warn stagger-in" style="animation-delay:45ms" href="index.php?action=tasks&tab=due">
|
||
<div class="stat-label">60 日内贷款到期</div>
|
||
<div class="stat-value num"><?= $dueSoonCount ?><small>笔</small></div>
|
||
<div class="stat-foot">需提前安排资金 →</div>
|
||
</a>
|
||
<a class="stat stat--link stat--gold stagger-in" style="animation-delay:90ms" href="index.php?action=tasks&tab=plan">
|
||
<div class="stat-label">待处理还款计划</div>
|
||
<div class="stat-value num"><?= $repaySoonCount ?><small>笔</small></div>
|
||
<div class="stat-foot">本金 / 利息到期未还 →</div>
|
||
</a>
|
||
<a class="stat stat--link stat--alert stagger-in" style="animation-delay:135ms" href="index.php?action=tasks&tab=over">
|
||
<div class="stat-label">贷款逾期</div>
|
||
<div class="stat-value num"><?= $overdueCount ?><small>笔</small></div>
|
||
<div class="stat-foot">已计逾期利息 →</div>
|
||
</a>
|
||
</div>
|
||
<div class="section-title" style="margin-top:0;">今日待办</div>
|
||
<div class="table-card">
|
||
<table>
|
||
<thead><tr><th style="width:96px;">日期</th><th>事项</th><th>关联</th><th>状态</th><th class="cell-right">操作</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($todos as $i => $t): ?>
|
||
<tr class="stagger-in" style="animation-delay:<?= min($i, 8) * 45 ?>ms; cursor:default;">
|
||
<td class="num"><?= e($t['date']) ?></td>
|
||
<td><div class="cell-main"><?= e($t['what']) ?></div></td>
|
||
<td class="num" style="white-space:normal;"><?= e($t['ref']) ?></td>
|
||
<td><span class="pill pill--<?= $t['pill'] ?>"><?= e($t['pillTxt']) ?></span></td>
|
||
<td class="cell-right"><a class="btn btn--plain btn--sm" href="index.php?action=tasks&tab=<?= e($t['tab']) ?>">去处理</a></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (!$todos): ?>
|
||
<tr><td colspan="5"><div class="empty" style="margin:14px;">今日暂无待办事项</div></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="section-title">快捷入口</div>
|
||
<div class="quick-dock">
|
||
<?php if ($isAdmin): ?>
|
||
<a class="qd-item stagger-in" href="index.php?action=archive_form">
|
||
<span class="qd-ico"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 2h5l3.5 3.5V16H5z"/><path d="M10 2v3.5h3.5"/><path d="M9 9v4M7 11h4"/></svg></span>
|
||
<span class="qd-main">新建档案</span>
|
||
<span class="qd-sub">五步录入 · 自动归档</span>
|
||
</a>
|
||
<button type="button" class="qd-item stagger-in" style="animation-delay:45ms" data-open-modal="import">
|
||
<span class="qd-ico"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2.5v7"/><path d="m6 6.5 3 3 3-3"/><path d="M3 11v3.5A1.5 1.5 0 0 0 4.5 16h9a1.5 1.5 0 0 0 1.5-1.5V11"/></svg></span>
|
||
<span class="qd-main">导入档案</span>
|
||
<span class="qd-sub">xlsx / csv 模板批量导入</span>
|
||
</button>
|
||
<?php endif; ?>
|
||
<a class="qd-item stagger-in" style="animation-delay:90ms" href="index.php?action=query_totals">
|
||
<span class="qd-ico"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 15.5h12"/><path d="M5.5 12V8M9 12V4.5M12.5 12V6.5"/></svg></span>
|
||
<span class="qd-main">统计查询</span>
|
||
<span class="qd-sub">总金额 / 应付本息 / 担保 / 历史</span>
|
||
</a>
|
||
<a class="qd-item stagger-in" style="animation-delay:135ms" href="index.php?action=interest_calculator">
|
||
<span class="qd-ico"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="6" r="2.2"/><circle cx="12" cy="12" r="2.2"/><path d="M13.5 4.5 4.5 13.5"/></svg></span>
|
||
<span class="qd-main">利息计算</span>
|
||
<span class="qd-sub">单利 · 360 / 365 天基准</span>
|
||
</a>
|
||
<a class="qd-item stagger-in" style="animation-delay:180ms" href="index.php?action=acceptance_discount_calculator">
|
||
<span class="qd-ico"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5.5h12A1.5 1.5 0 0 1 16.5 7v1.5a2 2 0 0 0 0 4V14A1.5 1.5 0 0 1 15 15.5H3A1.5 1.5 0 0 1 1.5 14v-1.5a2 2 0 0 0 0-4V7A1.5 1.5 0 0 1 3 5.5z"/><path d="M11 5.5v10" stroke-dasharray="2 2.5"/></svg></span>
|
||
<span class="qd-main">承兑贴现</span>
|
||
<span class="qd-sub">异地加天 · 节假日顺延</span>
|
||
</a>
|
||
</div>
|
||
<?php
|
||
pageFooter();
|
||
exit;
|
||
}
|
||
|
||
/* ============================================================
|
||
任务中心(样板 tasksPage:到期提醒与待更新合并,四组分段)
|
||
============================================================ */
|
||
if ($action === 'tasks') {
|
||
requireLogin();
|
||
$isAdmin = isAdmin();
|
||
$taskTab = (string)($_GET['tab'] ?? 'all');
|
||
if (!in_array($taskTab, ['all', 'due', 'plan', 'over', 'pending'], true)) {
|
||
$taskTab = 'all';
|
||
}
|
||
$groups = [];
|
||
|
||
$dueRows = [];
|
||
foreach ($dueSoonLoansFull as $r) {
|
||
$dueRows[] = [
|
||
'what' => (string)$r['end_date'] . ' 到期 · ' . archiveKindLabel(normalizeArchiveKind((string)($r['archive_kind'] ?? ''))),
|
||
'aid' => (int)$r['id'], 'plan_id' => '',
|
||
'name' => (string)$r['borrower'],
|
||
'amt' => $r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null,
|
||
'pill' => 'due', 'pillTxt' => '即将到期', 'kind' => 'due',
|
||
];
|
||
}
|
||
$groups[] = ['key' => 'due', 'name' => '贷款到期', 'rows' => $dueRows];
|
||
|
||
$planRows = [];
|
||
foreach ($repayPlanSoonFull as $r) {
|
||
$planRows[] = [
|
||
'what' => '还款计划到期 · ' . (string)$r['due_date'],
|
||
'aid' => (int)$r['archive_id'], 'plan_id' => (string)$r['plan_id'],
|
||
'name' => (string)$r['borrower'],
|
||
'amt' => (float)$r['amount'],
|
||
'pill' => 'due', 'pillTxt' => '未还', 'kind' => 'plan',
|
||
];
|
||
}
|
||
$groups[] = ['key' => 'plan', 'name' => '还款计划', 'rows' => $planRows];
|
||
|
||
$overRows = [];
|
||
foreach ($overdueLoansFull as $r) {
|
||
$overRows[] = [
|
||
'what' => '逾期贷款 · 应于 ' . (string)$r['end_date'],
|
||
'aid' => (int)$r['id'], 'plan_id' => '',
|
||
'name' => (string)$r['borrower'],
|
||
'amt' => $r['remaining_limit'] !== null ? (float)$r['remaining_limit'] : null,
|
||
'pill' => 'over', 'pillTxt' => '逾期', 'kind' => 'over',
|
||
];
|
||
}
|
||
foreach ($overduePlanRows as $r) {
|
||
$overRows[] = [
|
||
'what' => '逾期计划 · 应于 ' . (string)$r['due_date'],
|
||
'aid' => (int)$r['archive_id'], 'plan_id' => (string)$r['plan_id'],
|
||
'name' => (string)$r['borrower'],
|
||
'amt' => (float)$r['amount'],
|
||
'pill' => 'over', 'pillTxt' => '逾期', 'kind' => 'over_plan',
|
||
];
|
||
}
|
||
$groups[] = ['key' => 'over', 'name' => '逾期中', 'rows' => $overRows];
|
||
|
||
$pendRows = [];
|
||
foreach ($pendingUpdateRows as $r) {
|
||
$pendRows[] = [
|
||
'what' => '结果待确认 · ' . (string)$r['type_label'] . ' · ' . (string)$r['date'],
|
||
'aid' => (int)$r['archive_id'], 'plan_id' => (string)$r['plan_id'],
|
||
'name' => (string)$r['borrower'],
|
||
'amt' => isset($r['amount']) && $r['amount'] !== null ? (float)$r['amount'] : null,
|
||
'pill' => 'pending', 'pillTxt' => '待更新', 'kind' => ($r['row_kind'] ?? '') === 'loan' ? 'pending_loan' : 'pending_plan',
|
||
];
|
||
}
|
||
$groups[] = ['key' => 'pending', 'name' => '待更新', 'rows' => $pendRows];
|
||
|
||
$tabs = [['all', '全部'], ['due', '贷款到期'], ['plan', '还款计划'], ['over', '逾期中'], ['pending', '待更新']];
|
||
$exportType = ['all' => 'all', 'due' => 'due', 'plan' => 'plan', 'over' => 'overdue', 'pending' => 'all'][$taskTab];
|
||
pageHeader($me, $taskTotal);
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1 class="page-title">任务中心</h1>
|
||
<div class="page-sub">到期提醒与待更新合并处理 · 行内登记后剩余额度自动联动</div>
|
||
</div>
|
||
<div style="display:flex; gap:10px;">
|
||
<a class="btn btn--plain" href="index.php?action=reminders&type=<?= e($exportType) ?>&format=xlsx">导出 xlsx</a>
|
||
<a class="btn btn--plain" href="index.php?action=reminders&type=<?= e($exportType) ?>&format=csv">导出 csv</a>
|
||
</div>
|
||
</div>
|
||
<div class="seg" role="tablist" aria-label="任务分组">
|
||
<?php foreach ($tabs as [$k, $v]): ?>
|
||
<a role="tab" href="index.php?action=tasks&tab=<?= e($k) ?>" <?= $taskTab === $k ? 'data-active' : '' ?>><?= e($v) ?></a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php foreach ($groups as $gi => $g): ?>
|
||
<?php if ($taskTab !== 'all' && $g['key'] !== $taskTab) { continue; } ?>
|
||
<div class="section-title" <?= $gi === 0 ? 'style="margin-top:20px;"' : '' ?>><?= e($g['name']) ?><span class="nav-badge num"><?= count($g['rows']) ?></span></div>
|
||
<div class="table-card">
|
||
<table>
|
||
<thead><tr><th>事项</th><th>关联档案</th><th>借款主体</th><th class="cell-right">金额</th><th>状态</th><th class="cell-right">行内处理</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($g['rows'] as $i => $r): ?>
|
||
<tr class="stagger-in" style="animation-delay:<?= min($i, 8) * 45 ?>ms; cursor:default;">
|
||
<td><div class="cell-main"><?= e($r['what']) ?></div></td>
|
||
<td class="num"><a href="index.php?action=archive_detail&id=<?= $r['aid'] ?>">#<?= $r['aid'] ?></a></td>
|
||
<td><?= e($r['name']) ?></td>
|
||
<td class="cell-right num"><?= e(fmtMoney($r['amt'])) ?></td>
|
||
<td><span class="pill pill--<?= $r['pill'] ?>"><?= e($r['pillTxt']) ?></span></td>
|
||
<td class="cell-right">
|
||
<a class="btn btn--plain btn--sm" href="index.php?action=archive_detail&id=<?= $r['aid'] ?>">查看档案</a>
|
||
<?php if ($isAdmin && $r['kind'] === 'due'): ?>
|
||
<form method="post" action="index.php?action=reminder_action" class="inline" style="display:inline" onsubmit="return confirm('确认设置该档案为提前还款并标记已完成?')">
|
||
<input type="hidden" name="op" value="early_settle">
|
||
<input type="hidden" name="archive_id" value="<?= $r['aid'] ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn btn--plain btn--sm">提前还款</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php if ($isAdmin && $r['kind'] === 'plan'): ?>
|
||
<form method="post" action="index.php?action=reminder_action" class="inline" style="display:inline-flex;gap:6px;align-items:center">
|
||
<input type="hidden" name="op" value="plan_delay">
|
||
<input type="hidden" name="archive_id" value="<?= $r['aid'] ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e($r['plan_id']) ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<input type="date" name="new_due_date" required style="height:26px;font-size:12px;padding:0 6px;">
|
||
<button type="submit" class="btn btn--plain btn--sm">延期</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php if ($isAdmin && ($r['kind'] === 'over' || $r['kind'] === 'over_plan')): ?>
|
||
<form method="post" action="index.php?action=reminder_action" class="inline" style="display:inline" onsubmit="return confirm('确认将该逾期项标记为已归还?')">
|
||
<input type="hidden" name="op" value="overdue_paid">
|
||
<input type="hidden" name="archive_id" value="<?= $r['aid'] ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e($r['plan_id']) ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn btn--plain btn--sm">已归还</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php if ($isAdmin && $r['kind'] === 'pending_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="<?= $r['aid'] ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn btn--plain btn--sm">已还款</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="<?= $r['aid'] ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn btn--plain btn--sm">已逾期</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
<?php if ($isAdmin && $r['kind'] === 'pending_plan'): ?>
|
||
<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="<?= $r['aid'] ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e($r['plan_id']) ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn btn--plain btn--sm">已还款</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="<?= $r['aid'] ?>">
|
||
<input type="hidden" name="plan_id" value="<?= e($r['plan_id']) ?>">
|
||
<input type="hidden" name="redirect_to" value="<?= e(currentUrlFromServer()) ?>">
|
||
<button type="submit" class="btn btn--plain btn--sm">已逾期</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php if (!$g['rows']): ?>
|
||
<tr><td colspan="6"><div class="empty" style="margin:14px;">本组暂无待办</div></td></tr>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php
|
||
pageFooter();
|
||
exit;
|
||
}
|
||
|
||
/* ============================================================
|
||
档案库 · 目录管理(样板 dirsPage:三级目录三栏 + 所选目录档案预览)
|
||
============================================================ */
|
||
$view = (string)($_GET['view'] ?? 'files');
|
||
if ($action === 'home' && $view === 'dirs') {
|
||
requireLogin();
|
||
$isAdmin = isAdmin();
|
||
$d1 = (int)($_GET['d1'] ?? 0);
|
||
$d2 = (int)($_GET['d2'] ?? 0);
|
||
$d3 = (int)($_GET['d3'] ?? 0);
|
||
$byLevel = [1 => [], 2 => [], 3 => []];
|
||
$byParent = [];
|
||
$dirById = [];
|
||
foreach ($dirs as $d) {
|
||
$dirById[(int)$d['id']] = $d;
|
||
$byLevel[(int)$d['level']][] = $d;
|
||
$byParent[(int)($d['parent_id'] ?? 0)][] = $d;
|
||
}
|
||
if (!isset($dirById[$d1])) { $d1 = 0; }
|
||
if (!isset($dirById[$d2]) || (int)$dirById[$d2]['parent_id'] !== $d1) { $d2 = 0; }
|
||
if (!isset($dirById[$d3]) || (int)$dirById[$d3]['parent_id'] !== $d2) { $d3 = 0; }
|
||
if ($d1 === 0 && !empty($byLevel[1])) {
|
||
$d1 = (int)$byLevel[1][0]['id'];
|
||
}
|
||
$l2nodes = $d1 ? array_values($byParent[$d1] ?? []) : null;
|
||
if ($d2 === 0 && $l2nodes) {
|
||
$d2 = (int)$l2nodes[0]['id'];
|
||
}
|
||
$l3nodes = $d2 ? array_values($byParent[$d2] ?? []) : null;
|
||
$dirArchiveCount = function (int $id) use ($rows, $dirs): int {
|
||
$ids = array_flip(collectSubDirIds($dirs, $id));
|
||
$n = 0;
|
||
foreach ($rows as $r) {
|
||
if (isset($ids[(int)$r['directory_id']])) {
|
||
$n++;
|
||
}
|
||
}
|
||
return $n;
|
||
};
|
||
$dirCol = function (int $level, string $title, ?array $nodes, int $selId, string $parentHint) use ($isAdmin, $d1, $d2, $d3, $dirArchiveCount): string {
|
||
$names = [1 => '一级目录', 2 => '二级目录', 3 => '三级目录'];
|
||
$addHref = 'index.php?action=new_dir' . ($level === 2 ? '&parent_id=' . $d1 : ($level === 3 ? '&parent_id=' . $d2 : ''));
|
||
$addDisabled = $nodes === null;
|
||
$html = '<div class="dir-col"><div class="dir-col-head">'
|
||
. '<div class="dir-col-title">' . $names[$level] . '<small>' . e($title) . ' · ' . ($nodes === null ? 0 : count($nodes)) . ' 项</small></div>';
|
||
if ($isAdmin) {
|
||
$html .= $addDisabled
|
||
? '<button class="btn btn--plain btn--sm" disabled>+ 新增</button>'
|
||
: '<a class="btn btn--plain btn--sm" href="' . e($addHref) . '">+ 新增</a>';
|
||
}
|
||
$html .= '</div>';
|
||
if ($nodes === null) {
|
||
return $html . '<div class="dir-empty">' . e($parentHint) . '</div></div>';
|
||
}
|
||
if (!$nodes) {
|
||
return $html . '<div class="dir-empty">暂无' . $names[$level] . ($isAdmin ? ',可点「+ 新增」创建' : '') . '</div></div>';
|
||
}
|
||
foreach ($nodes as $n) {
|
||
$id = (int)$n['id'];
|
||
$qs = ['view' => 'dirs'];
|
||
if ($level >= 1) { $qs['d1'] = $level === 1 ? $id : $d1; }
|
||
if ($level >= 2) { $qs['d2'] = $level === 2 ? $id : $d2; }
|
||
if ($level >= 3) { $qs['d3'] = $id; }
|
||
$href = 'index.php?' . http_build_query($qs);
|
||
$name = (string)$n['name'];
|
||
$html .= '<div class="dir-row-wrap"' . ($id === $selId ? ' data-active' : '') . '>'
|
||
. '<a class="dir-row" href="' . e($href) . '" title="查看下级"'
|
||
. ($id === $selId ? ' aria-current="true"' : '')
|
||
. ' aria-label="查看下级:' . e($name) . '">'
|
||
. '<span class="dir-name">' . e($name) . '</span>'
|
||
. '<span class="nav-badge num">' . $dirArchiveCount($id) . '</span>'
|
||
. '</a>';
|
||
if ($isAdmin) {
|
||
$html .= '<span class="dir-acts">'
|
||
. '<a href="index.php?action=edit_dir&id=' . $id . '" title="改名">改名</a>'
|
||
. '<a href="index.php?action=delete_dir&id=' . $id . '" title="删除" onclick="return confirm(\'仅空目录可删除,确认?\')">删除</a>'
|
||
. '</span>';
|
||
}
|
||
$html .= '</div>';
|
||
}
|
||
return $html . '</div>';
|
||
};
|
||
$selDirId = $d3 ?: ($d2 ?: $d1);
|
||
$previewRows = [];
|
||
if ($selDirId) {
|
||
$scope = array_flip(collectSubDirIds($dirs, $selDirId));
|
||
foreach ($rows as $r) {
|
||
if (isset($scope[(int)$r['directory_id']])) {
|
||
$previewRows[] = $r;
|
||
}
|
||
}
|
||
}
|
||
$selPath = $selDirId ? (string)($dirPath[$selDirId] ?? '') : '';
|
||
pageHeader($me, $taskTotal);
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1 class="page-title">档案库</h1>
|
||
<div class="page-sub">三级目录归档 · 一级机构主体 / 二级业务类型 / 三级贷款机构</div>
|
||
<div class="seg head-switch" role="tablist" aria-label="档案库视图切换">
|
||
<a role="tab" href="index.php">档案列表</a>
|
||
<a role="tab" href="index.php?view=dirs" data-active>目录管理</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="dir-hint">点击目录行查看下级;<?= $isAdmin ? '新增、改名、删除直接作用于目录树。' : '目录维护由管理员进行。' ?>列表「目录路径」、按目录筛选与新增档案第 1 步均使用同一套目录</div>
|
||
<div class="dir-cols">
|
||
<?= $dirCol(1, '机构主体', array_values($byLevel[1]), $d1, '') ?>
|
||
<?= $dirCol(2, '业务类型', $l2nodes, $d2, '请先选择左侧一级目录') ?>
|
||
<?= $dirCol(3, '贷款机构', $l3nodes, $d3, '请先选择左侧二级目录') ?>
|
||
</div>
|
||
<?php if ($selDirId): ?>
|
||
<div class="section-title">所选目录档案<span class="nav-badge num"><?= count($previewRows) ?></span></div>
|
||
<div class="filter-note" style="margin:-8px 0 10px;"><?= e($selPath) ?></div>
|
||
<?php if ($previewRows): ?>
|
||
<div class="table-card">
|
||
<table>
|
||
<thead><tr><th>档案名称</th><th>借款人</th><th>贷款机构</th><th>类型</th><th class="cell-right">剩余额度</th><th>状态</th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($previewRows as $a):
|
||
$s = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id'])); ?>
|
||
<tr data-open-detail="<?= (int)$a['id'] ?>" tabindex="0" role="button" aria-label="查看 <?= e((string)$a['borrower']) ?> 档案详情">
|
||
<td><div class="cell-main"><?= e((string)$a['name']) ?></div></td>
|
||
<td><?= e((string)$a['borrower']) ?></td>
|
||
<td><?= e((string)$a['loan_institution']) ?></td>
|
||
<td><?= e(archiveKindLabel(normalizeArchiveKind((string)($a['archive_kind'] ?? '')))) ?></td>
|
||
<td class="cell-right num cell-remaining"><?= e(fmtMoney($a['remaining_limit'] !== null ? (float)$a['remaining_limit'] : null)) ?></td>
|
||
<td><span class="pill pill--<?= statusPillCls($s) ?>"><?= e($s) ?></span></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="filter-bar" style="margin:12px 0 0;">
|
||
<span class="filter-note">共 <?= count($previewRows) ?> 笔</span>
|
||
<span class="filter-spacer"></span>
|
||
<a class="btn btn--plain" href="index.php?dir_id=<?= $selDirId ?>">在档案列表中查看 ›</a>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="empty-action">
|
||
<div class="ea-title">该目录下暂无档案</div>
|
||
新增档案时在第 1 步选择此三级目录,即可归档到此处
|
||
<?php if ($isAdmin): ?><br><a class="btn btn--plain" href="index.php?action=archive_form<?= $selDirId ? '&dir_id=' . $selDirId : '' ?>">+ 新建档案</a><?php endif; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
<?php
|
||
pageFooter();
|
||
exit;
|
||
}
|
||
|
||
/* ============================================================
|
||
档案库 · 档案列表(样板 listPage:统计卡 + 分段筛选 + 表格 + 抽屉)
|
||
============================================================ */
|
||
$sumAmount = 0.0;
|
||
$sumRemaining = 0.0;
|
||
$ongoingCount = 0;
|
||
foreach ($archives as $a) {
|
||
$st = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
|
||
if ($st === '已完成') {
|
||
continue;
|
||
}
|
||
$ongoingCount++;
|
||
$sumAmount += (float)($a['credit_limit'] ?? 0);
|
||
$sumRemaining += (float)($a['remaining_limit'] ?? 0);
|
||
}
|
||
pageHeader($me, $taskTotal);
|
||
?>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1 class="page-title">档案库</h1>
|
||
<div class="page-sub">金牛集团 · 银行贷款与对外担保归档</div>
|
||
<div class="seg head-switch" role="tablist" aria-label="档案库视图切换">
|
||
<a role="tab" href="index.php" data-active>档案列表</a>
|
||
<a role="tab" href="index.php?view=dirs">目录管理</a>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
|
||
<form method="get" action="index.php" class="search-cluster">
|
||
<?php if ($dirId): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<input type="hidden" name="kind" value="<?= e($kindFilter) ?>">
|
||
<input type="hidden" name="status" value="<?= e($statusFilter) ?>">
|
||
<input type="hidden" name="sort_by" value="<?= e($sortBy) ?>">
|
||
<input type="hidden" name="sort_order" value="<?= e($sortOrder) ?>">
|
||
<input type="hidden" name="per_page" value="<?= $perPage ?>">
|
||
<input type="hidden" name="search_field" value="<?= e($searchField) ?>">
|
||
<div class="seg" role="group" aria-label="搜索范围">
|
||
<a href="<?= e($listUrl(['search_field' => 'all', 'page' => null])) ?>" data-search-scope="all" <?= $searchField === 'all' ? 'data-active' : '' ?>>全部</a>
|
||
<a href="<?= e($listUrl(['search_field' => 'borrower', 'page' => null])) ?>" data-search-scope="borrower" <?= $searchField === 'borrower' ? 'data-active' : '' ?>>借款人</a>
|
||
<a href="<?= e($listUrl(['search_field' => 'guarantor', 'page' => null])) ?>" data-search-scope="guarantor" <?= $searchField === 'guarantor' ? 'data-active' : '' ?>>担保人</a>
|
||
</div>
|
||
<div class="search"><span aria-hidden="true">⌕</span><input type="search" name="q" value="<?= e($q) ?>" placeholder="<?= e($searchPlaceholder) ?>" aria-label="搜索档案"></div>
|
||
</form>
|
||
<?php if (isAdmin()): ?><button type="button" class="btn btn--plain" data-open-modal="import">导入</button><?php endif; ?>
|
||
<a class="btn btn--plain" href="<?= e($listUrl(['action' => 'export', 'mode' => 'all', 'format' => 'xlsx'])) ?>">导出</a>
|
||
<?php if (isAdmin()): ?><a class="btn btn--primary" href="index.php?action=archive_form<?= $dirId ? '&dir_id=' . $dirId : '' ?>">+ 新建档案</a><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="stats">
|
||
<div class="stat stat--gold"><div class="stat-label">在保剩余额度</div><div class="stat-value num"><?= e(fmtWan($sumRemaining)) ?></div><div class="stat-foot">授信合计 <?= e(fmtWan($sumAmount)) ?></div></div>
|
||
<div class="stat"><div class="stat-label">在保档案</div><div class="stat-value num"><?= $ongoingCount ?><small>笔</small></div><div class="stat-foot">含对外担保 · 待更新 <?= $pendingUpdateCount ?> 笔</div></div>
|
||
<a class="stat stat--link" href="index.php?action=tasks&tab=due"><div class="stat-label">60 日内到期</div><div class="stat-value num"><?= $dueSoonCount ?><small>笔</small></div><div class="stat-foot">需提前安排资金 →</div></a>
|
||
<a class="stat stat--link stat--alert" href="index.php?action=tasks&tab=over"><div class="stat-label">逾期中</div><div class="stat-value num"><?= $overdueCount ?><small>笔</small></div><div class="stat-foot">已计逾期利息 →</div></a>
|
||
</div>
|
||
<div class="seg" role="tablist" aria-label="业务类型筛选">
|
||
<a role="tab" href="<?= e($listUrl(['kind' => 'all', 'page' => null])) ?>" <?= $kindFilter === 'all' ? 'data-active' : '' ?>>全部</a>
|
||
<a role="tab" href="<?= e($listUrl(['kind' => ARCHIVE_KIND_BANK_LOAN, 'page' => null])) ?>" <?= $kindFilter === ARCHIVE_KIND_BANK_LOAN ? 'data-active' : '' ?>>银行贷款</a>
|
||
<a role="tab" href="<?= e($listUrl(['kind' => ARCHIVE_KIND_EXTERNAL_GUARANTEE, 'page' => null])) ?>" <?= $kindFilter === ARCHIVE_KIND_EXTERNAL_GUARANTEE ? 'data-active' : '' ?>>对外担保</a>
|
||
</div>
|
||
<form method="get" action="index.php" class="filter-bar" style="margin-top:12px;">
|
||
<div class="dir-picker" data-dir-picker data-selected="<?= $dirId ?>" data-dirs="<?= e(json_encode($dirPickerItems, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>">
|
||
<input type="hidden" name="dir_id" value="<?= $dirId ?>">
|
||
<button type="button" class="dir-picker-trigger" data-dir-picker-trigger aria-haspopup="tree" aria-expanded="false" aria-controls="jn-dir-picker" aria-label="按归档目录筛选" title="<?= e($dirPickerLabel) ?>">
|
||
<span class="dir-picker-trigger-text"><?= e($dirPickerLabel) ?></span>
|
||
</button>
|
||
</div>
|
||
<input type="hidden" name="kind" value="<?= e($kindFilter) ?>">
|
||
<input type="hidden" name="search_field" value="<?= e($searchField) ?>">
|
||
<select name="status" aria-label="按状态筛选" onchange="this.form.submit()">
|
||
<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" aria-label="排序方式" onchange="this.form.submit()">
|
||
<option value="end_date" <?= $sortBy === 'end_date' ? 'selected' : '' ?>>按到期日</option>
|
||
<option value="created_at" <?= $sortBy === 'created_at' ? 'selected' : '' ?>>按创建时间</option>
|
||
<option value="loan_institution" <?= $sortBy === 'loan_institution' ? 'selected' : '' ?>>按贷款机构</option>
|
||
</select>
|
||
<input type="hidden" name="sort_order" value="<?= e($sortOrder) ?>">
|
||
<div class="seg" role="group" aria-label="排序方向">
|
||
<a href="<?= e($listUrl(['sort_order' => 'asc', 'page' => null])) ?>" <?= $sortOrder === 'asc' ? 'data-active' : '' ?>>正序</a>
|
||
<a href="<?= e($listUrl(['sort_order' => 'desc', 'page' => null])) ?>" <?= $sortOrder === 'desc' ? 'data-active' : '' ?>>倒序</a>
|
||
</div>
|
||
<input type="hidden" name="per_page" value="<?= $perPage ?>">
|
||
<input type="hidden" name="q" value="<?= e($q) ?>">
|
||
<span class="filter-note"><?= $totalCount ?> 笔结果<?= $dirId ? ' · 目录筛选中' : '' ?><?= $searchField !== 'all' && $q !== '' ? ' · ' . ($searchField === 'borrower' ? '借款人' : '担保人') : '' ?></span>
|
||
<?php if ($q !== '' || $statusFilter !== 'not_done' || $dirId || $searchField !== 'all'): ?>
|
||
<span class="filter-spacer"></span>
|
||
<a class="btn btn--plain btn--sm" href="index.php">清除全部筛选</a>
|
||
<?php endif; ?>
|
||
</form>
|
||
<form method="get" action="index.php" id="batch-export-form">
|
||
<input type="hidden" name="action" value="export">
|
||
<input type="hidden" name="mode" value="selected">
|
||
<input type="hidden" name="format" value="xlsx">
|
||
<input type="hidden" name="kind" value="<?= e($kindFilter) ?>">
|
||
<?php if ($dirId): ?><input type="hidden" name="dir_id" value="<?= $dirId ?>"><?php endif; ?>
|
||
<div data-batch-slot style="display:none;">
|
||
<div class="batch-bar">
|
||
已选 <b class="num" data-batch-count>0</b> 笔
|
||
<span style="flex:1"></span>
|
||
<button type="submit" class="btn btn--plain">批量导出</button>
|
||
<?php if (isAdmin()): ?><button type="button" class="btn btn--danger" onclick="requestDeleteSelected()">申请删除</button><?php endif; ?>
|
||
<button type="button" class="btn btn--plain btn--sm" data-clear-check>取消选择</button>
|
||
</div>
|
||
</div>
|
||
<div class="table-card">
|
||
<table>
|
||
<thead><tr>
|
||
<th class="cell-check"><input type="checkbox" data-check-all aria-label="全选当前筛选结果"></th>
|
||
<th>档案名称</th><th>借款人</th><th>贷款机构</th><th>类型</th>
|
||
<th class="cell-right">授信额度</th><th class="cell-right">剩余额度</th>
|
||
<th class="cell-right">年利率</th><th>到期日</th><th>状态</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<?php foreach ($archivesPage as $i => $a):
|
||
$s = archiveStatusWithConfig($a['end_date'], getArchiveFinanceConfig($financeAllGlobal, (int)$a['id']));
|
||
$rowKind = normalizeArchiveKind($a['archive_kind'] ?? '');
|
||
$stKey = ['履行中' => 'ok', '逾期中' => 'over', '待更新' => 'pending', '已完成' => 'done'][$s] ?? 'pending';
|
||
?>
|
||
<tr class="stagger-in" style="animation-delay:<?= min($i, 8) * 45 ?>ms" data-open-detail="<?= (int)$a['id'] ?>" data-status="<?= $stKey ?>" tabindex="0" role="button" aria-label="查看 <?= e((string)$a['borrower']) ?> 档案详情">
|
||
<td class="cell-check"><input type="checkbox" name="ids[]" value="<?= (int)$a['id'] ?>" data-check="<?= (int)$a['id'] ?>" aria-label="选择档案 <?= (int)$a['id'] ?>" tabindex="-1"></td>
|
||
<td><div class="cell-main"><?= e((string)$a['name']) ?></div><div class="cell-sub"><?= e((string)($dirPath[(int)$a['directory_id']] ?? '')) ?></div></td>
|
||
<td><?= e((string)$a['borrower']) ?></td>
|
||
<td><?= e((string)$a['loan_institution']) ?></td>
|
||
<td><?= e(archiveKindLabel($rowKind)) ?></td>
|
||
<td class="cell-right num"><?= e(fmtMoney($a['credit_limit'] !== null ? (float)$a['credit_limit'] : null)) ?></td>
|
||
<td class="cell-right num cell-remaining"><?= e(fmtMoney($a['remaining_limit'] !== null ? (float)$a['remaining_limit'] : null)) ?></td>
|
||
<td class="cell-right num"><?= $a['annual_rate'] === null ? '—' : e(number_format((float)$a['annual_rate'], 2)) . '%' ?></td>
|
||
<td class="num"><?= e((string)$a['end_date']) ?></td>
|
||
<td><span class="pill pill--<?= statusPillCls($s) ?>"><?= e($s) ?></span></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php if (!$archivesPage): ?>
|
||
<div style="padding:16px;">
|
||
<div class="empty-action">
|
||
<div class="ea-title">当前筛选无匹配档案</div>
|
||
尝试调整目录、搜索范围或状态筛选,或清空关键词
|
||
<br><a class="btn btn--plain" href="index.php">清除全部筛选</a>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</form>
|
||
<script>
|
||
function requestDeleteSelected(){
|
||
var ids=[].slice.call(document.querySelectorAll('[data-check]:checked')).map(function(x){return x.value;});
|
||
if(ids.length===0){if(window.jnToast)window.jnToast('请先选择要删除的档案');return;}
|
||
if(!confirm('将提交双管理员删除申请,确认?')){return;}
|
||
window.location.href='index.php?action=request_delete_selected&ids='+encodeURIComponent(ids.join(','));
|
||
}
|
||
</script>
|
||
<div class="filter-bar" style="margin:12px 0 0;">
|
||
<span class="filter-note">第 <?= $currentPage ?> / <?= $totalPages ?> 页 · 共 <?= $totalCount ?> 条</span>
|
||
<span class="filter-spacer"></span>
|
||
<?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 class="btn btn--plain btn--sm" href="index.php?<?= $prevQuery ?>">‹ 上一页</a><?php endif; ?>
|
||
<form method="get" action="index.php" style="display:inline-flex;gap:6px;align-items:center;margin:0;">
|
||
<?php foreach ($baseQuery as $bk => $bv): if ($bk === 'per_page') { continue; } ?><input type="hidden" name="<?= e((string)$bk) ?>" value="<?= e((string)$bv) ?>"><?php endforeach; ?>
|
||
<select name="per_page" onchange="this.form.submit()" aria-label="每页条数" style="height:26px;font-size:12px;padding:0 6px;">
|
||
<?php foreach ([10, 15, 30, 50] as $pp): ?>
|
||
<option value="<?= $pp ?>" <?= $perPage === $pp ? 'selected' : '' ?>>每页 <?= $pp ?> 条</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</form>
|
||
<?php if ($currentPage < $totalPages): ?><a class="btn btn--plain btn--sm" href="index.php?<?= $nextQuery ?>">下一页 ›</a><?php endif; ?>
|
||
</div>
|
||
<?php
|
||
pageFooter();
|