feat: 初始化财务管理应用前端项目,包含账户、预算、交易、报表、设置等核心功能模块。
This commit is contained in:
@@ -5,25 +5,52 @@
|
||||
import api from './api';
|
||||
import type { Account, AccountFormInput, TransferFormInput, ApiResponse } from '../types';
|
||||
|
||||
/**
|
||||
* Map account data from API (snake_case) to frontend (camelCase)
|
||||
*/
|
||||
function mapAccountFromApi(data: Record<string, unknown>): Account {
|
||||
return {
|
||||
id: data.id as number,
|
||||
name: data.name as string,
|
||||
type: data.type as Account['type'],
|
||||
balance: data.balance as number,
|
||||
currency: data.currency as Account['currency'],
|
||||
icon: data.icon as string,
|
||||
isCredit: data.is_credit as boolean,
|
||||
billingDate: data.billing_date as number | undefined,
|
||||
paymentDate: data.payment_date as number | undefined,
|
||||
createdAt: data.created_at as string,
|
||||
updatedAt: data.updated_at as string,
|
||||
sortOrder: data.sort_order as number,
|
||||
warningThreshold: data.warning_threshold as number | undefined,
|
||||
lastSyncTime: data.last_sync_time as string | undefined,
|
||||
accountCode: data.account_code as string | undefined,
|
||||
accountType: data.account_type as 'asset' | 'liability' | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all accounts
|
||||
*/
|
||||
export async function getAccounts(): Promise<Account[]> {
|
||||
const response = await api.get<ApiResponse<Account[]>>('/accounts');
|
||||
return response.data || [];
|
||||
const response = await api.get<ApiResponse<Record<string, unknown>[]>>('/accounts');
|
||||
const rawData = response.data || [];
|
||||
return rawData.map(mapAccountFromApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single account by ID
|
||||
*/
|
||||
export async function getAccount(id: number): Promise<Account> {
|
||||
const response = await api.get<ApiResponse<Account>>(`/accounts/${id}`);
|
||||
const response = await api.get<ApiResponse<Record<string, unknown>>>(`/accounts/${id}`);
|
||||
if (!response.data) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
return response.data;
|
||||
return mapAccountFromApi(response.data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a new account
|
||||
*/
|
||||
|
||||
@@ -25,35 +25,65 @@ export interface PiggyBankTransactionInput {
|
||||
note?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map piggy bank data from API (snake_case) to frontend (camelCase)
|
||||
*/
|
||||
function mapPiggyBankFromApi(data: Record<string, unknown>): PiggyBank {
|
||||
return {
|
||||
id: data.id as number,
|
||||
name: data.name as string,
|
||||
targetAmount: data.target_amount as number,
|
||||
currentAmount: data.current_amount as number,
|
||||
type: data.type as PiggyBankType,
|
||||
targetDate: data.target_date as string | undefined,
|
||||
linkedAccountId: data.linked_account_id as number | undefined,
|
||||
autoRule: data.auto_rule as string | undefined,
|
||||
progress: ((data.current_amount as number) / (data.target_amount as number)) * 100 || 0,
|
||||
createdAt: data.created_at as string,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all piggy banks
|
||||
*/
|
||||
export async function getPiggyBanks(): Promise<PiggyBank[]> {
|
||||
const response = await api.get<ApiResponse<PiggyBank[]>>('/piggy-banks');
|
||||
return response.data || [];
|
||||
const response = await api.get<ApiResponse<Record<string, unknown>[]>>('/piggy-banks');
|
||||
const rawData = response.data || [];
|
||||
return rawData.map(mapPiggyBankFromApi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single piggy bank by ID
|
||||
*/
|
||||
export async function getPiggyBank(id: number): Promise<PiggyBank> {
|
||||
const response = await api.get<ApiResponse<PiggyBank>>(`/piggy-banks/${id}`);
|
||||
const response = await api.get<ApiResponse<Record<string, unknown>>>(`/piggy-banks/${id}`);
|
||||
if (!response.data) {
|
||||
throw new Error('Piggy bank not found');
|
||||
}
|
||||
return response.data;
|
||||
return mapPiggyBankFromApi(response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new piggy bank
|
||||
*/
|
||||
export async function createPiggyBank(data: PiggyBankFormInput): Promise<PiggyBank> {
|
||||
// Convert target_date to RFC3339 format (Go expects full timestamp)
|
||||
let formattedTargetDate: string | undefined;
|
||||
if (data.targetDate) {
|
||||
// If it's just YYYY-MM-DD, append T00:00:00Z to make it RFC3339 compliant
|
||||
if (data.targetDate.length === 10 && !data.targetDate.includes('T')) {
|
||||
formattedTargetDate = `${data.targetDate}T00:00:00Z`;
|
||||
} else {
|
||||
formattedTargetDate = data.targetDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert camelCase to snake_case for backend
|
||||
const payload = {
|
||||
name: data.name,
|
||||
target_amount: data.targetAmount,
|
||||
type: data.type,
|
||||
target_date: data.targetDate,
|
||||
target_date: formattedTargetDate,
|
||||
linked_account_id: data.linkedAccountId,
|
||||
auto_rule: data.autoRule,
|
||||
};
|
||||
@@ -71,15 +101,25 @@ export async function updatePiggyBank(
|
||||
id: number,
|
||||
data: Partial<PiggyBankFormInput>
|
||||
): Promise<PiggyBank> {
|
||||
// Convert target_date to RFC3339 format (Go expects full timestamp)
|
||||
let formattedTargetDate: string | undefined;
|
||||
if (data.targetDate !== undefined) {
|
||||
if (data.targetDate && data.targetDate.length === 10 && !data.targetDate.includes('T')) {
|
||||
formattedTargetDate = `${data.targetDate}T00:00:00Z`;
|
||||
} else {
|
||||
formattedTargetDate = data.targetDate;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert camelCase to snake_case for backend
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (data.name !== undefined) payload.name = data.name;
|
||||
if (data.targetAmount !== undefined) payload.target_amount = data.targetAmount;
|
||||
if (data.type !== undefined) payload.type = data.type;
|
||||
if (data.targetDate !== undefined) payload.target_date = data.targetDate;
|
||||
if (data.targetDate !== undefined) payload.target_date = formattedTargetDate;
|
||||
if (data.linkedAccountId !== undefined) payload.linked_account_id = data.linkedAccountId;
|
||||
if (data.autoRule !== undefined) payload.auto_rule = data.autoRule;
|
||||
|
||||
|
||||
const response = await api.put<ApiResponse<PiggyBank>>(`/piggy-banks/${id}`, payload);
|
||||
if (!response.data) {
|
||||
throw new Error(response.error || 'Failed to update piggy bank');
|
||||
@@ -199,25 +239,25 @@ export function estimateCompletionDate(
|
||||
1,
|
||||
Math.ceil((now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24))
|
||||
);
|
||||
|
||||
|
||||
// Prevent division issues
|
||||
if (!isFinite(daysElapsed) || daysElapsed <= 0) return null;
|
||||
|
||||
|
||||
const dailyRate = currentAmount / daysElapsed;
|
||||
if (!isFinite(dailyRate) || dailyRate <= 0) return null;
|
||||
|
||||
|
||||
const remainingAmount = targetAmount - currentAmount;
|
||||
const daysRemaining = Math.ceil(remainingAmount / dailyRate);
|
||||
|
||||
|
||||
// Sanity check: don't estimate more than 10 years out
|
||||
if (!isFinite(daysRemaining) || daysRemaining > 3650) return null;
|
||||
|
||||
const estimatedDate = new Date(now);
|
||||
estimatedDate.setDate(estimatedDate.getDate() + daysRemaining);
|
||||
|
||||
|
||||
// Final validation
|
||||
if (isNaN(estimatedDate.getTime())) return null;
|
||||
|
||||
|
||||
return estimatedDate.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user