feat: phase 3 - ledger and asset management

Add group expense tracking (ledger) and public asset inventory (asset) features.
Ledger supports income/expense recording with monthly summary. Asset tracks
group equipment with free-form holder transfer. Both are independent pages
accessible from GroupView navigation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
congsh
2026-04-18 19:42:04 +08:00
parent 625d0baf7d
commit c5413644f9
21 changed files with 4407 additions and 1 deletions
+132
View File
@@ -0,0 +1,132 @@
// src/api/ledgers.ts
import { pb } from './pocketbase'
import type { Ledger } from '@/types'
interface CreateLedgerData {
group: string
type: string
amount: number
category: string
description: string
relatedMembers?: string[]
occurredAt: string
}
interface ListLedgersOptions {
page?: number
limit?: number
type?: string
category?: string
month?: string
}
interface LedgerSummary {
totalIncome: number
totalExpense: number
balance: number
}
export async function createLedger(data: CreateLedgerData): Promise<Ledger> {
const user = pb.authStore.model
const record = await pb.collection('ledgers').create({
...data,
creator: user?.id || '',
relatedMembers: data.relatedMembers || []
})
return record as unknown as Ledger
}
export async function listLedgers(
groupId: string,
options?: ListLedgersOptions
): Promise<{ items: Ledger[]; total: number }> {
const { page = 1, limit = 30, type, category, month } = options || {}
let filter = `group="${groupId}"`
if (type) filter += ` && type="${type}"`
if (category) filter += ` && category="${category}"`
if (month) {
// month format: "2026-04"
const start = `${month}-01 00:00:00`
const year = parseInt(month.slice(0, 4))
const mon = parseInt(month.slice(5, 7))
// 下个月的第一天
const nextMonth = mon === 12 ? `${year + 1}-01` : `${year}-${String(mon + 1).padStart(2, '0')}`
const end = `${nextMonth}-01 00:00:00`
filter += ` && occurredAt>="${start}" && occurredAt<"${end}"`
}
const result = await pb.collection('ledgers').getList(page, limit, {
filter,
sort: '-occurredAt',
expand: 'creator,relatedMembers'
})
return { items: result.items as unknown as Ledger[], total: result.totalItems }
}
export async function updateLedger(
ledgerId: string,
data: Partial<CreateLedgerData>
): Promise<Ledger> {
const record = await pb.collection('ledgers').update(ledgerId, data)
return record as unknown as Ledger
}
export async function deleteLedger(ledgerId: string): Promise<void> {
await pb.collection('ledgers').delete(ledgerId)
}
export async function getLedgerSummary(
groupId: string,
month?: string
): Promise<LedgerSummary> {
let totalIncome = 0
let totalExpense = 0
let page = 1
const batchSize = 500
let hasMore = true
while (hasMore) {
let filter = `group="${groupId}"`
if (month) {
const start = `${month}-01 00:00:00`
const year = parseInt(month.slice(0, 4))
const mon = parseInt(month.slice(5, 7))
const nextMonth = mon === 12 ? `${year + 1}-01` : `${year}-${String(mon + 1).padStart(2, '0')}`
const end = `${nextMonth}-01 00:00:00`
filter += ` && occurredAt>="${start}" && occurredAt<"${end}"`
}
const result = await pb.collection('ledgers').getList(page, batchSize, {
filter,
fields: 'type,amount'
})
for (const item of result.items as any[]) {
if (item.type === 'income') {
totalIncome += item.amount || 0
} else if (item.type === 'expense') {
totalExpense += item.amount || 0
}
}
hasMore = result.items.length === batchSize && page * batchSize < result.totalItems
page++
}
return {
totalIncome,
totalExpense,
balance: totalIncome - totalExpense
}
}
export function subscribeLedgers(
groupId: string,
callback: (data: any) => void
) {
return pb.collection('ledgers').subscribe('*', (data) => {
if (data.record?.group === groupId) {
callback(data)
}
})
}