feat: add fee workspace and polish AI presentation
This commit is contained in:
169
modules/billing/types.ts
Normal file
169
modules/billing/types.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
export const BILLING_STATUS_VALUES = ["paid", "partial", "pending", "overdue"] as const;
|
||||
export type BillingStatus = (typeof BILLING_STATUS_VALUES)[number];
|
||||
|
||||
export const BILLING_STATUS_LABELS: Record<BillingStatus, string> = {
|
||||
paid: "已结清",
|
||||
partial: "部分收款",
|
||||
pending: "待收款",
|
||||
overdue: "已逾期",
|
||||
};
|
||||
|
||||
export const PAYMENT_METHOD_VALUES = ["wechat", "alipay", "bank_transfer", "card", "cash"] as const;
|
||||
export type PaymentMethod = (typeof PAYMENT_METHOD_VALUES)[number];
|
||||
|
||||
export const PAYMENT_METHOD_LABELS: Record<PaymentMethod, string> = {
|
||||
wechat: "微信支付",
|
||||
alipay: "支付宝",
|
||||
bank_transfer: "银行转账",
|
||||
card: "银行卡",
|
||||
cash: "现金",
|
||||
};
|
||||
|
||||
export const INVOICE_STATUS_VALUES = ["not_requested", "pending", "issued"] as const;
|
||||
export type InvoiceStatus = (typeof INVOICE_STATUS_VALUES)[number];
|
||||
|
||||
export const INVOICE_STATUS_LABELS: Record<InvoiceStatus, string> = {
|
||||
not_requested: "未开票",
|
||||
pending: "待开票",
|
||||
issued: "已开票",
|
||||
};
|
||||
|
||||
export type BillingCharge = {
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
quantity: number;
|
||||
serviceDate: string;
|
||||
unitPriceCents: number;
|
||||
};
|
||||
|
||||
export type BillingPayment = {
|
||||
id: string;
|
||||
amountCents: number;
|
||||
method: PaymentMethod;
|
||||
paidAt: string;
|
||||
reference: string;
|
||||
operator: string;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type BillingStatement = {
|
||||
id: string;
|
||||
elderId: string;
|
||||
elderName: string;
|
||||
roomLabel: string;
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
period: string;
|
||||
dueAt: string;
|
||||
charges: BillingCharge[];
|
||||
payments: BillingPayment[];
|
||||
invoiceStatus: InvoiceStatus;
|
||||
invoiceNumber?: string;
|
||||
receiptCount: number;
|
||||
};
|
||||
|
||||
export type BillingStatementView = BillingStatement & {
|
||||
totalCents: number;
|
||||
paidCents: number;
|
||||
outstandingCents: number;
|
||||
status: BillingStatus;
|
||||
};
|
||||
|
||||
export type BillingSummary = {
|
||||
receivableCents: number;
|
||||
collectedCents: number;
|
||||
outstandingCents: number;
|
||||
overdueCount: number;
|
||||
};
|
||||
|
||||
export function getChargeAmountCents(charge: BillingCharge): number {
|
||||
return Math.round(charge.quantity * charge.unitPriceCents);
|
||||
}
|
||||
|
||||
export function getStatementTotalCents(statement: BillingStatement): number {
|
||||
return statement.charges.reduce((total, charge) => total + getChargeAmountCents(charge), 0);
|
||||
}
|
||||
|
||||
export function getStatementPaidCents(statement: BillingStatement): number {
|
||||
return statement.payments.reduce((total, payment) => total + payment.amountCents, 0);
|
||||
}
|
||||
|
||||
export function getStatementOutstandingCents(statement: BillingStatement): number {
|
||||
return Math.max(0, getStatementTotalCents(statement) - getStatementPaidCents(statement));
|
||||
}
|
||||
|
||||
export function getBillingStatus(statement: BillingStatement, referenceDate = new Date()): BillingStatus {
|
||||
const outstandingCents = getStatementOutstandingCents(statement);
|
||||
if (outstandingCents === 0) {
|
||||
return "paid";
|
||||
}
|
||||
|
||||
if (new Date(statement.dueAt).getTime() < referenceDate.getTime()) {
|
||||
return "overdue";
|
||||
}
|
||||
|
||||
return statement.payments.length > 0 ? "partial" : "pending";
|
||||
}
|
||||
|
||||
export function toBillingStatementView(
|
||||
statement: BillingStatement,
|
||||
referenceDate = new Date(),
|
||||
): BillingStatementView {
|
||||
const totalCents = getStatementTotalCents(statement);
|
||||
const paidCents = getStatementPaidCents(statement);
|
||||
return {
|
||||
...statement,
|
||||
totalCents,
|
||||
paidCents,
|
||||
outstandingCents: Math.max(0, totalCents - paidCents),
|
||||
status: getBillingStatus(statement, referenceDate),
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateBillingSummary(
|
||||
statements: readonly BillingStatement[],
|
||||
referenceDate = new Date(),
|
||||
): BillingSummary {
|
||||
return statements.reduce<BillingSummary>(
|
||||
(summary, statement) => {
|
||||
const view = toBillingStatementView(statement, referenceDate);
|
||||
return {
|
||||
receivableCents: summary.receivableCents + view.totalCents,
|
||||
collectedCents: summary.collectedCents + view.paidCents,
|
||||
outstandingCents: summary.outstandingCents + view.outstandingCents,
|
||||
overdueCount: summary.overdueCount + (view.status === "overdue" ? 1 : 0),
|
||||
};
|
||||
},
|
||||
{ receivableCents: 0, collectedCents: 0, outstandingCents: 0, overdueCount: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
export function addBillingPayment(statement: BillingStatement, payment: BillingPayment): BillingStatement {
|
||||
return {
|
||||
...statement,
|
||||
payments: [payment, ...statement.payments],
|
||||
};
|
||||
}
|
||||
|
||||
export function addBillingCharge(statement: BillingStatement, charge: BillingCharge): BillingStatement {
|
||||
return {
|
||||
...statement,
|
||||
charges: [...statement.charges, charge],
|
||||
invoiceStatus: "not_requested",
|
||||
invoiceNumber: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatCny(cents: number): string {
|
||||
return new Intl.NumberFormat("zh-CN", {
|
||||
style: "currency",
|
||||
currency: "CNY",
|
||||
minimumFractionDigits: 2,
|
||||
}).format(cents / 100);
|
||||
}
|
||||
|
||||
export function formatBillingPeriod(period: string): string {
|
||||
const [year, month] = period.split("-");
|
||||
return year && month ? `${year}年${month}月` : period;
|
||||
}
|
||||
Reference in New Issue
Block a user