- Add transactions_dirty table and zero-tolerance cleansing action - Implement bookkeeping workbench (/bookkeeping) with searchable Combobox channel selector and card suffix / account identifier - Implement transaction ledger (/transactions) with date groupings, multi-currency metrics, and filters - Update AGENTS.md guidelines for Base UI Select and Combobox bindings
120 lines
3.1 KiB
TypeScript
120 lines
3.1 KiB
TypeScript
"use server"
|
|
|
|
import { and, desc, eq, isNull } from "drizzle-orm"
|
|
import { revalidatePath } from "next/cache"
|
|
import { auth } from "@/lib/auth"
|
|
import { db } from "@/lib/db"
|
|
import { channels, transactions, type Transaction } from "@/lib/db/schema"
|
|
|
|
async function requireUser() {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
throw new Error("请先登录")
|
|
}
|
|
return session.user.id
|
|
}
|
|
|
|
export type TransactionWithChannelDetails = Transaction & {
|
|
channelDetails: {
|
|
id: string
|
|
cardBrand: string | null
|
|
channelType: string
|
|
displayName: string
|
|
region: string | null
|
|
cardNumberSuffix: string | null
|
|
}[]
|
|
}
|
|
|
|
export async function getOfficialTransactionsAction(): Promise<{
|
|
success: boolean
|
|
data?: TransactionWithChannelDetails[]
|
|
error?: string
|
|
}> {
|
|
try {
|
|
const userId = await requireUser()
|
|
|
|
const officialTxns = await db
|
|
.select()
|
|
.from(transactions)
|
|
.where(
|
|
and(eq(transactions.userId, userId), isNull(transactions.deletedAt))
|
|
)
|
|
.orderBy(desc(transactions.txnDate), desc(transactions.createdAt))
|
|
|
|
const userChannels = await db
|
|
.select()
|
|
.from(channels)
|
|
.where(and(eq(channels.userId, userId), isNull(channels.deletedAt)))
|
|
|
|
const channelMap = new Map(userChannels.map((c) => [c.id, c]))
|
|
|
|
const result: TransactionWithChannelDetails[] = officialTxns.map((t) => {
|
|
const chDetails: TransactionWithChannelDetails["channelDetails"] = []
|
|
if (Array.isArray(t.refChannels)) {
|
|
for (const chId of t.refChannels) {
|
|
const ch = channelMap.get(chId)
|
|
if (ch) {
|
|
const name = ch.desc || ch.issuerName || ch.platform || "渠道"
|
|
chDetails.push({
|
|
id: ch.id,
|
|
cardBrand: ch.cardBrand,
|
|
channelType: ch.channelType,
|
|
displayName: name,
|
|
region: ch.region,
|
|
cardNumberSuffix: ch.cardNumberSuffix,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
...t,
|
|
channelDetails: chDetails,
|
|
}
|
|
})
|
|
|
|
return { success: true, data: result }
|
|
} catch (err) {
|
|
console.error("getOfficialTransactionsAction error:", err)
|
|
return {
|
|
success: false,
|
|
error: err instanceof Error ? err.message : "获取交易记录失败",
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function deleteOfficialTransactionAction(
|
|
id: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const userId = await requireUser()
|
|
const [deleted] = await db
|
|
.update(transactions)
|
|
.set({
|
|
deletedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(transactions.id, id),
|
|
eq(transactions.userId, userId),
|
|
isNull(transactions.deletedAt)
|
|
)
|
|
)
|
|
.returning()
|
|
|
|
if (!deleted) {
|
|
return { success: false, error: "交易不存在或已删除" }
|
|
}
|
|
|
|
revalidatePath("/transactions")
|
|
revalidatePath("/")
|
|
return { success: true }
|
|
} catch (err) {
|
|
console.error("deleteOfficialTransactionAction error:", err)
|
|
return {
|
|
success: false,
|
|
error: err instanceof Error ? err.message : "删除交易失败",
|
|
}
|
|
}
|
|
}
|