Files
FamilyBook/templates/ledger/index.php
T
laoyang 8f80f3e835 v24 第 2 轮:19 条清单收尾(附件预览/CalDAV/任务分类看板/全局选人/家谱/纪念日/财务)
用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:

- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
     弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
     REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
     按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
     修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
     压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
     以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
     (实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
     金额一律以「分」存,不用浮点。

验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
2026-10-03 08:18:05 +08:00

288 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 财务 / 账本(v24 用户第 15 条)
*
* 用户原话:「先创建一个家庭,或者说,创建一个账本,这个账本,可以共享给某个人,
* 共享后,这个人就可以往这个账本加入账目,这样,双方都可以看到所有的账目,
* 最后计算出总数,这就是一个家庭的总账目。」
*
* 页面结构就是照着这句话来的:
* 顶上一排账本(切换 / 新建)→ 汇总卡(资产 / 负债 / 净资产)→ 账目表 → 共享设置。
*
* ⚠️ 金额全程以**分**参与计算(Ledger 里做的),这里只负责显示成元。
* 任何地方都不要拿元去加总。
*
* @param array $books 我能看到的账本(带 my_perm / entry_count)
* @param array $cur 当前账本
* @param array $entries 当前账本的账目
* @param array $summary ['asset','debt','net','by_kind','count'](单位:分)
* @param array $shares 当前账本的共享列表
* @param array $people 名册
*/
$canWrite = \App\Auth::canWrite() && \App\Ledger::canWrite($cur);
$curId = (int)$cur['id'];
$money = static fn($c): string => \App\Ledger::money($c);
?>
<div class="page-head">
<h1>财务</h1>
<span class="sub">家庭的账 · 共享后双方都能记、都能看</span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('ledger', ['book' => $curId])) ?>">刷新</a>
</div>
</div>
<?php if (count($books) > 1): ?>
<div class="seg">
<?php foreach ($books as $b): ?>
<a class="seg-btn<?= (int)$b['id'] === $curId ? ' is-on' : '' ?>"
href="<?= e(url('ledger', ['book' => (int)$b['id']])) ?>">
<?= e((string)$b['name']) ?><?= ($b['my_perm'] ?? '') === 'read' ? '(只读)' : '' ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="card card-pad led-sum">
<div class="led-sum-head">
<h2 class="cinema-h2" style="margin:0"><?= e((string)$cur['name']) ?></h2>
<span class="small muted">
<?= (int)($cur['entry_count'] ?? count($entries)) ?> 笔账
<?php if (($cur['my_perm'] ?? '') === 'read'): ?>
· <b>只读</b>(不能记账)
<?php endif; ?>
</span>
</div>
<div class="led-total">
<div class="led-total-item">
<span class="led-total-lb">总资产</span>
<span class="led-total-num led-asset"><?= e($money($summary['asset'])) ?></span>
</div>
<div class="led-total-item">
<span class="led-total-lb">总负债</span>
<span class="led-total-num led-debt"><?= e($money($summary['debt'])) ?></span>
</div>
<div class="led-total-item led-net">
<span class="led-total-lb">净资产</span>
<span class="led-total-num"><?= e($money($summary['net'])) ?></span>
</div>
</div>
<?php if ($summary['by_kind']): ?>
<div class="led-kinds">
<?php foreach ($summary['by_kind'] as $k => $c): ?>
<span class="led-kind-chip<?= \App\Ledger::isDebt((string)$k) ? ' is-debt' : '' ?>">
<?= \App\Ledger::kindIcon((string)$k) ?> <?= e(\App\Ledger::kindWord((string)$k)) ?>
<b><?= e($money($c)) ?></b>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($canWrite): ?>
<div class="card card-pad">
<h2 class="cinema-h2">记一笔</h2>
<form method="post" action="<?= e(url('ledger/entry_add')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
<div class="form-grid">
<div class="field">
<label for="le-kind">类别 *</label>
<select class="select" id="le-kind" name="kind">
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
<option value="<?= e($k) ?>"><?= e($v[2] . ' ' . $v[0]) ?><?= $v[1] === 'debt' ? '(负债)' : '' ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label for="le-title">名称 *</label>
<input class="input" id="le-title" name="title" required placeholder="如:工商银行定期">
</div>
<div class="field">
<label for="le-amount">金额(元)*</label>
<input class="input" id="le-amount" name="amount" required inputmode="decimal"
placeholder="如:120000.00">
<span class="tip">负债(贷款/信用卡)也填正数,系统按类别算成负债。</span>
</div>
<div class="field">
<label for="le-person">挂在谁名下</label>
<select class="select" id="le-person" name="person_id">
<option value="0">不指定</option>
<?php foreach ($people as $p): ?>
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="field full">
<label for="le-note">备注</label>
<input class="input" id="le-note" name="note" placeholder="可选">
</div>
</div>
<div class="btn-row"><button class="btn" type="submit">添加账目</button></div>
</form>
</div>
<?php endif; ?>
<div class="card card-pad">
<h2 class="cinema-h2">账目(<?= count($entries) ?>)</h2>
<?php if (!$entries): ?>
<p class="empty-hint">这本账还没有账目。上面「记一笔」开始记吧。</p>
<?php else: ?>
<div class="table-wrap">
<table class="table">
<thead>
<tr><th>类别</th><th>名称</th><th class="num">金额</th><th>挂在谁名下</th><th>备注</th><th></th></tr>
</thead>
<tbody>
<?php foreach ($entries as $en): ?>
<?php $ek = (string)$en['kind']; $debt = \App\Ledger::isDebt($ek); ?>
<tr>
<td class="nowrap">
<span class="led-kind-chip<?= $debt ? ' is-debt' : '' ?>">
<?= \App\Ledger::kindIcon($ek) ?> <?= e(\App\Ledger::kindWord($ek)) ?>
</span>
</td>
<td><?= e((string)$en['title']) ?></td>
<td class="num<?= $debt ? ' led-debt' : ' led-asset' ?>">
<?= $debt ? '-' : '' ?><?= e($money((int)$en['amount_cents'])) ?>
</td>
<td class="nowrap">
<?php if (!empty($en['person_name'])): ?>
<?= person_chip([
'id' => (int)$en['person_id'], 'name' => (string)$en['person_name'],
'my_nickname' => (string)($en['person_nick'] ?? ''),
'avatar' => $en['avatar'] ?? null, 'avatar_color' => $en['avatar_color'] ?? null,
]) ?>
<?php else: ?><span class="small muted">—</span><?php endif; ?>
</td>
<td class="small muted"><?= e((string)($en['note'] ?? '')) ?></td>
<td class="nowrap">
<?php if ($canWrite): ?>
<details class="led-edit">
<summary class="link-quiet">改</summary>
<form method="post" action="<?= e(url('ledger/entry_update')) ?>" class="node-form">
<?= csrf_field() ?>
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
<input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
<div class="nf-grid">
<div class="field">
<label>类别</label>
<select class="select" name="kind">
<?php foreach (\App\Ledger::KINDS as $k => $v): ?>
<option value="<?= e($k) ?>" <?= $ek === $k ? 'selected' : '' ?>>
<?= e($v[0]) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label>名称</label>
<input class="input" name="title" value="<?= e((string)$en['title']) ?>" required>
</div>
<div class="field">
<label>金额(元)</label>
<input class="input" name="amount"
value="<?= e(\App\Ledger::amountYuan((int)$en['amount_cents'])) ?>" required>
</div>
<div class="field">
<label>备注</label>
<input class="input" name="note" value="<?= e((string)($en['note'] ?? '')) ?>">
</div>
</div>
<div class="btn-row"><button class="btn btn-sm" type="submit">保存</button></div>
</form>
<form method="post" action="<?= e(url('ledger/entry_del')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
<input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit"
onclick="return confirm('删掉这笔账目?')">删除</button>
</form>
</details>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th colspan="2">合计(资产 − 负债)</th>
<th class="num led-net"><?= e($money($summary['net'])) ?></th>
<th colspan="3"></th>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
</div>
<div class="card card-pad">
<h2 class="cinema-h2">共享与账本</h2>
<div class="led-shares">
<span class="res-label">已共享给</span>
<?php if (!$shares): ?>
<span class="small muted">还没有共享给任何人。</span>
<?php endif; ?>
<?php foreach ($shares as $s): ?>
<span class="led-share-chip">
<?= person_chip([
'id' => (int)$s['person_id'], 'name' => (string)$s['name'],
'avatar' => $s['avatar'] ?? null, 'avatar_color' => $s['avatar_color'] ?? null,
]) ?>
<span class="node-badge"><?= ($s['perm'] ?? 'read') === 'write' ? '可写' : '只读' ?></span>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('ledger/unshare')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
<input type="hidden" name="person_id" value="<?= (int)$s['person_id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit">取消</button>
</form>
<?php endif; ?>
</span>
<?php endforeach; ?>
</div>
<?php if ($canWrite): ?>
<div class="led-ops">
<?php
$ppName = 'person_id';
$ppPeople = $people;
$ppSelected = [];
$ppMode = 'single';
$ppOpenBtn = '选择共享给谁';
$ppTitle = '账本共享给谁';
$ppEmptyText = '未选择';
?>
<form class="inline-form" method="post" action="<?= e(url('ledger/share')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
<?php require __DIR__ . '/../_person_picker.php'; ?>
<select class="select" name="perm">
<?php foreach (\App\Ledger::PERM as $pk => $pv): ?>
<option value="<?= e($pk) ?>"><?= e($pv) ?></option>
<?php endforeach; ?>
</select>
<button class="btn btn-sm" type="submit">共享</button>
</form>
<form class="inline-form" method="post" action="<?= e(url('ledger/book_rename')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
<input class="input" name="name" value="<?= e((string)$cur['name']) ?>" required>
<button class="btn btn-ghost btn-sm" type="submit">账本改名</button>
</form>
<form class="inline-form" method="post" action="<?= e(url('ledger/book_add')) ?>">
<?= csrf_field() ?>
<input class="input" name="name" placeholder="新账本名,如:孩子的教育金" required>
<button class="btn btn-ghost btn-sm" type="submit">新建账本</button>
</form>
<form class="inline-form" method="post" action="<?= e(url('ledger/book_del')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="ledger_id" value="<?= $curId ?>">
<button class="btn btn-ghost btn-sm" type="submit"
onclick="return confirm('删掉这本账?里面的账目会一起删掉。')">删除这本账</button>
</form>
</div>
<?php endif; ?>
</div>