Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 103 additions & 100 deletions portal/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import type {
Account,
Balance,
Transaction,
TransactionSplit,
Attachment,
DataFeedConnection,
Category
Category,
Budget,
BudgetCurrentPeriod,
BudgetReport,
Reconciliation,
ReconciliationWithTransactions
} from './types';

const API_BASE = '';
Expand Down Expand Up @@ -177,100 +180,14 @@ export async function deleteTransaction(id: string) {
return api<void>(`/api/transactions/${id}`, { method: 'DELETE' });
}

// ── Splits ─────────────────────────────────────────────────────────────────

export async function listSplits(transactionId: string) {
return api<{ splits: TransactionSplit[] }>(`/api/transactions/${transactionId}/splits`);
}

export async function createSplit(
transactionId: string,
data: {
amountMinor: number;
currency: string;
categoryId?: string;
description?: string;
memo?: string;
sortOrder?: number;
}
) {
return api<TransactionSplit>(`/api/transactions/${transactionId}/splits`, {
method: 'POST',
body: JSON.stringify(data)
});
}

export async function updateSplit(
transactionId: string,
splitId: string,
data: {
amountMinor?: number;
currency?: string;
categoryId?: string | null;
description?: string | null;
memo?: string | null;
sortOrder?: number;
}
) {
return api<TransactionSplit>(`/api/transactions/${transactionId}/splits/${splitId}`, {
method: 'PATCH',
body: JSON.stringify(data)
});
}

export async function deleteSplit(transactionId: string, splitId: string) {
return api<void>(`/api/transactions/${transactionId}/splits/${splitId}`, { method: 'DELETE' });
}

// ── Attachments ────────────────────────────────────────────────────────────

export async function listTransactionAttachments(transactionId: string) {
return api<{ attachments: Attachment[] }>(`/api/transactions/${transactionId}/attachments`);
}

export async function uploadTransactionAttachment(
transactionId: string,
file: File,
kind: string = 'other'
) {
const form = new FormData();
form.append('file', file);
form.append('kind', kind);
return api<Attachment>(`/api/transactions/${transactionId}/attachments`, {
method: 'POST',
body: form
});
}

export async function uploadSplitAttachment(
transactionId: string,
splitId: string,
file: File,
kind: string = 'other'
) {
const form = new FormData();
form.append('file', file);
form.append('kind', kind);
return api<Attachment>(`/api/transactions/${transactionId}/splits/${splitId}/attachments`, {
method: 'POST',
body: form
});
}

export function getAttachmentUrl(attachmentId: string) {
return `/api/attachments/${attachmentId}`;
}

export async function deleteAttachment(attachmentId: string) {
return api<void>(`/api/attachments/${attachmentId}`, { method: 'DELETE' });
}

// ── Connections ────────────────────────────────────────────────────────────

export async function listConnections() {
return api<{ connections: DataFeedConnection[] }>('/api/connections');
}

// ── Categories ─────────────────────────────────────────────────────────────

export async function listCategories() {
return api<{ categories: Category[] }>('/api/categories');
}
Expand Down Expand Up @@ -303,15 +220,12 @@ export async function patchOnboarding(data: {

// ── Budgets ────────────────────────────────────────────────────────────────

export interface Budget {
id: string;
name: string;
period: string;
currency: string;
style: string;
income: { amount: number; currencyCode: string } | null;
startsOn: string;
isActive: boolean;
export async function listBudgets() {
return api<{ budgets: Budget[] }>('/api/budgets');
}

export async function getBudget(id: string) {
return api<Budget>(`/api/budgets/${id}`);
}

export async function createBudget(data: {
Expand All @@ -328,3 +242,92 @@ export async function createBudget(data: {
});
}

export async function createPeriod(
budgetId: string,
data: { startDate: string; allocations: { categoryId: string; amountMinor: number }[] }
) {
return api<BudgetCurrentPeriod>(`/api/budgets/${budgetId}/periods`, {
method: 'POST',
body: JSON.stringify(data)
});
}

export async function updateAllocation(
budgetId: string,
periodId: string,
categoryId: string,
data: { amountMinor: number; rolloverEnabled?: boolean }
) {
return api<{ success: boolean }>(
`/api/budgets/${budgetId}/periods/${periodId}/categories/${categoryId}`,
{
method: 'PATCH',
body: JSON.stringify(data)
}
);
}

export async function closePeriod(budgetId: string, periodId: string) {
return api<{
closedPeriodId: string;
nextPeriodId: string;
rolloverBalances: { categoryId: string; rolloverAmountMinor: number; currency: string }[];
}>(`/api/budgets/${budgetId}/periods/${periodId}/close`, { method: 'POST' });
}

export async function getBudgetReport(budgetId: string, periodId: string, displayCurrency?: string) {
const qs = displayCurrency ? `?displayCurrency=${displayCurrency}` : '';
return api<BudgetReport>(`/api/budgets/${budgetId}/periods/${periodId}/report${qs}`);
}

export async function getCurrentBudgetReport(budgetId: string, displayCurrency?: string) {
const qs = displayCurrency ? `?displayCurrency=${displayCurrency}` : '';
return api<BudgetReport>(`/api/budgets/${budgetId}/periods/current/report${qs}`);
}

// ── Reconciliations ────────────────────────────────────────────────────────

export async function listReconciliations() {
return api<{ reconciliations: Reconciliation[] }>('/api/reconciliations');
}

export async function getReconciliation(id: string) {
return api<ReconciliationWithTransactions>(`/api/reconciliations/${id}`);
}

export async function createReconciliation(data: {
accountId: string;
statementDate: string;
statementBalanceMinor: number;
currency: string;
}) {
return api<{ reconciliation: Reconciliation; candidateTransactions: Transaction[] }>(
'/api/reconciliations',
{
method: 'POST',
body: JSON.stringify(data)
}
);
}

export async function updateReconciliationTransactions(
id: string,
data: { included: string[]; excluded: string[] }
) {
return api<{ success: boolean }>(`/api/reconciliations/${id}/transactions`, {
method: 'PATCH',
body: JSON.stringify(data)
});
}

export async function completeReconciliation(id: string, force = false) {
return api<{ status: string; diffMinor: number }>(
`/api/reconciliations/${id}/complete?force=${force}`,
{ method: 'POST' }
);
}

export async function abortReconciliation(id: string) {
return api<{ status: string }>(`/api/reconciliations/${id}/abort`, { method: 'POST' });
}

82 changes: 82 additions & 0 deletions portal/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,85 @@ export interface Category {
name: string;
color: string | null;
}

// ── Budgets ────────────────────────────────────────────────────────────────

export type BudgetPeriod = 'monthly' | 'biweekly' | 'weekly';
export type BudgetStyle = 'zeroBased' | 'envelope' | 'flexible' | 'traditionalLimits';

export interface BudgetPeriodCategory {
categoryId: string;
allocatedMinor: number;
openingBalanceMinor: number;
rolloverBalanceMinor: number;
currency: string;
rolloverEnabled: boolean;
}

export interface BudgetCurrentPeriod {
id: string;
startDate: string;
endDate: string;
status: 'Open' | 'Closed';
allocations: BudgetPeriodCategory[];
}

export interface Budget {
id: string;
name: string;
period: BudgetPeriod;
currency: string;
style: BudgetStyle;
incomeMinor: number;
isActive: boolean;
startsOn: string;
currentPeriod: BudgetCurrentPeriod | null;
}

export interface BudgetReportTotals {
allocatedMinor: number;
spentMinor: number;
remainingMinor: number;
currency: string;
}

export interface BudgetReportCategoryItem {
categoryId: string;
name: string;
allocatedMinor: number;
spentMinor: number;
remainingMinor: number;
rolloverBalanceMinor: number;
percentUsed: number;
currency: string;
}

export interface BudgetReport {
periodId: string;
totals: BudgetReportTotals;
byCategory: BudgetReportCategoryItem[];
displayCurrency: string;
}

// ── Reconciliations ────────────────────────────────────────────────────────

export type ReconciliationStatus = 'open' | 'completed' | 'aborted';

export interface Reconciliation {
id: string;
accountId: string;
statementDate: string;
statementBalanceMinor: number;
currency: string;
status: ReconciliationStatus;
note: string | null;
createdByUserId: string;
startedAt: string;
completedAt: string | null;
}

export interface ReconciliationWithTransactions extends Reconciliation {
includedTransactions: Transaction[];
candidateTransactions: Transaction[];
diffMinor: number;
}
6 changes: 5 additions & 1 deletion portal/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@
<h1 class="text-2xl font-semibold text-gray-900">Steward</h1>
<p class="text-sm text-gray-500">Welcome back, {auth.user.displayName}</p>
</div>
<a href="/logout" class="text-sm text-blue-600 hover:underline">Log out</a>
<div class="flex items-center gap-4">
<a href="/budgets" class="text-sm text-gray-600 hover:text-gray-900">Budgets</a>
<a href="/reconciliation" class="text-sm text-gray-600 hover:text-gray-900">Reconcile</a>
<a href="/logout" class="text-sm text-blue-600 hover:underline">Log out</a>
</div>
</header>

{#if onboarding && onboarding.currentStep < 5}
Expand Down
Loading
Loading