mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 13:26:42 +02:00
chore: archive finance, mail, moodlit apps and rename voxel-lava
- Move finance, mail, moodlit to apps-archived for later development - Rename games/voxel-lava to games/voxelava 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c3c272abc9
commit
ace7fa8f7f
427 changed files with 0 additions and 0 deletions
249
apps-archived/finance/packages/shared/src/constants/index.ts
Normal file
249
apps-archived/finance/packages/shared/src/constants/index.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import type { AccountType, CategoryType } from '../types';
|
||||
|
||||
// Account Type Labels
|
||||
export const ACCOUNT_TYPE_LABELS: Record<AccountType, { de: string; en: string }> = {
|
||||
checking: { de: 'Girokonto', en: 'Checking Account' },
|
||||
savings: { de: 'Sparkonto', en: 'Savings Account' },
|
||||
credit_card: { de: 'Kreditkarte', en: 'Credit Card' },
|
||||
cash: { de: 'Bargeld', en: 'Cash' },
|
||||
investment: { de: 'Investment', en: 'Investment' },
|
||||
loan: { de: 'Kredit', en: 'Loan' },
|
||||
other: { de: 'Sonstiges', en: 'Other' },
|
||||
};
|
||||
|
||||
// Account Type Icons
|
||||
export const ACCOUNT_TYPE_ICONS: Record<AccountType, string> = {
|
||||
checking: 'bank',
|
||||
savings: 'piggy-bank',
|
||||
credit_card: 'credit-card',
|
||||
cash: 'wallet',
|
||||
investment: 'chart-line-up',
|
||||
loan: 'hand-coins',
|
||||
other: 'dots-three',
|
||||
};
|
||||
|
||||
// Account Type Colors
|
||||
export const ACCOUNT_TYPE_COLORS: Record<AccountType, string> = {
|
||||
checking: '#3b82f6',
|
||||
savings: '#22c55e',
|
||||
credit_card: '#f97316',
|
||||
cash: '#8b5cf6',
|
||||
investment: '#06b6d4',
|
||||
loan: '#ef4444',
|
||||
other: '#6b7280',
|
||||
};
|
||||
|
||||
// Default Categories
|
||||
export interface DefaultCategory {
|
||||
name: { de: string; en: string };
|
||||
type: CategoryType;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_CATEGORIES: DefaultCategory[] = [
|
||||
// Expense Categories
|
||||
{
|
||||
name: { de: 'Lebensmittel', en: 'Groceries' },
|
||||
type: 'expense',
|
||||
color: '#22c55e',
|
||||
icon: 'shopping-cart',
|
||||
},
|
||||
{
|
||||
name: { de: 'Restaurant', en: 'Dining' },
|
||||
type: 'expense',
|
||||
color: '#f97316',
|
||||
icon: 'fork-knife',
|
||||
},
|
||||
{ name: { de: 'Transport', en: 'Transport' }, type: 'expense', color: '#3b82f6', icon: 'car' },
|
||||
{ name: { de: 'Wohnen', en: 'Housing' }, type: 'expense', color: '#8b5cf6', icon: 'house' },
|
||||
{
|
||||
name: { de: 'Versicherungen', en: 'Insurance' },
|
||||
type: 'expense',
|
||||
color: '#6b7280',
|
||||
icon: 'shield-check',
|
||||
},
|
||||
{ name: { de: 'Gesundheit', en: 'Health' }, type: 'expense', color: '#ef4444', icon: 'heart' },
|
||||
{
|
||||
name: { de: 'Unterhaltung', en: 'Entertainment' },
|
||||
type: 'expense',
|
||||
color: '#ec4899',
|
||||
icon: 'game-controller',
|
||||
},
|
||||
{
|
||||
name: { de: 'Shopping', en: 'Shopping' },
|
||||
type: 'expense',
|
||||
color: '#eab308',
|
||||
icon: 'shopping-bag',
|
||||
},
|
||||
{
|
||||
name: { de: 'Bildung', en: 'Education' },
|
||||
type: 'expense',
|
||||
color: '#6366f1',
|
||||
icon: 'graduation-cap',
|
||||
},
|
||||
{ name: { de: 'Reisen', en: 'Travel' }, type: 'expense', color: '#06b6d4', icon: 'airplane' },
|
||||
{
|
||||
name: { de: 'Abonnements', en: 'Subscriptions' },
|
||||
type: 'expense',
|
||||
color: '#a855f7',
|
||||
icon: 'repeat',
|
||||
},
|
||||
{
|
||||
name: { de: 'Sonstiges', en: 'Other Expense' },
|
||||
type: 'expense',
|
||||
color: '#9ca3af',
|
||||
icon: 'dots-three',
|
||||
},
|
||||
|
||||
// Income Categories
|
||||
{ name: { de: 'Gehalt', en: 'Salary' }, type: 'income', color: '#22c55e', icon: 'money' },
|
||||
{
|
||||
name: { de: 'Nebeneinkommen', en: 'Side Income' },
|
||||
type: 'income',
|
||||
color: '#3b82f6',
|
||||
icon: 'briefcase',
|
||||
},
|
||||
{
|
||||
name: { de: 'Investitionen', en: 'Investments' },
|
||||
type: 'income',
|
||||
color: '#8b5cf6',
|
||||
icon: 'chart-line-up',
|
||||
},
|
||||
{ name: { de: 'Geschenke', en: 'Gifts' }, type: 'income', color: '#ec4899', icon: 'gift' },
|
||||
{
|
||||
name: { de: 'Sonstiges', en: 'Other Income' },
|
||||
type: 'income',
|
||||
color: '#9ca3af',
|
||||
icon: 'dots-three',
|
||||
},
|
||||
];
|
||||
|
||||
// Supported Currencies
|
||||
export interface Currency {
|
||||
code: string;
|
||||
name: { de: string; en: string };
|
||||
symbol: string;
|
||||
decimalDigits: number;
|
||||
}
|
||||
|
||||
export const CURRENCIES: Currency[] = [
|
||||
{ code: 'EUR', name: { de: 'Euro', en: 'Euro' }, symbol: '€', decimalDigits: 2 },
|
||||
{ code: 'USD', name: { de: 'US-Dollar', en: 'US Dollar' }, symbol: '$', decimalDigits: 2 },
|
||||
{
|
||||
code: 'GBP',
|
||||
name: { de: 'Britisches Pfund', en: 'British Pound' },
|
||||
symbol: '£',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'CHF',
|
||||
name: { de: 'Schweizer Franken', en: 'Swiss Franc' },
|
||||
symbol: 'CHF',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'JPY',
|
||||
name: { de: 'Japanischer Yen', en: 'Japanese Yen' },
|
||||
symbol: '¥',
|
||||
decimalDigits: 0,
|
||||
},
|
||||
{
|
||||
code: 'CAD',
|
||||
name: { de: 'Kanadischer Dollar', en: 'Canadian Dollar' },
|
||||
symbol: 'C$',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'AUD',
|
||||
name: { de: 'Australischer Dollar', en: 'Australian Dollar' },
|
||||
symbol: 'A$',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'CNY',
|
||||
name: { de: 'Chinesischer Yuan', en: 'Chinese Yuan' },
|
||||
symbol: '¥',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'INR',
|
||||
name: { de: 'Indische Rupie', en: 'Indian Rupee' },
|
||||
symbol: '₹',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'PLN',
|
||||
name: { de: 'Polnischer Zloty', en: 'Polish Zloty' },
|
||||
symbol: 'zł',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'SEK',
|
||||
name: { de: 'Schwedische Krone', en: 'Swedish Krona' },
|
||||
symbol: 'kr',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'NOK',
|
||||
name: { de: 'Norwegische Krone', en: 'Norwegian Krone' },
|
||||
symbol: 'kr',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
{
|
||||
code: 'DKK',
|
||||
name: { de: 'Dänische Krone', en: 'Danish Krone' },
|
||||
symbol: 'kr',
|
||||
decimalDigits: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_CURRENCY = 'EUR';
|
||||
|
||||
// Recurrence Frequencies
|
||||
export const RECURRENCE_FREQUENCIES = [
|
||||
{ value: 'daily', label: { de: 'Täglich', en: 'Daily' } },
|
||||
{ value: 'weekly', label: { de: 'Wöchentlich', en: 'Weekly' } },
|
||||
{ value: 'biweekly', label: { de: 'Zweiwöchentlich', en: 'Biweekly' } },
|
||||
{ value: 'monthly', label: { de: 'Monatlich', en: 'Monthly' } },
|
||||
{ value: 'yearly', label: { de: 'Jährlich', en: 'Yearly' } },
|
||||
] as const;
|
||||
|
||||
// Date Formats
|
||||
export const DATE_FORMATS = [
|
||||
{ value: 'dd.MM.yyyy', label: 'DD.MM.YYYY (31.12.2024)' },
|
||||
{ value: 'MM/dd/yyyy', label: 'MM/DD/YYYY (12/31/2024)' },
|
||||
{ value: 'yyyy-MM-dd', label: 'YYYY-MM-DD (2024-12-31)' },
|
||||
{ value: 'dd/MM/yyyy', label: 'DD/MM/YYYY (31/12/2024)' },
|
||||
] as const;
|
||||
|
||||
// Week Start Options
|
||||
export const WEEK_START_OPTIONS = [
|
||||
{ value: 0, label: { de: 'Sonntag', en: 'Sunday' } },
|
||||
{ value: 1, label: { de: 'Montag', en: 'Monday' } },
|
||||
] as const;
|
||||
|
||||
// Budget Alert Thresholds
|
||||
export const BUDGET_ALERT_THRESHOLDS = [
|
||||
{ value: '0.50', label: '50%' },
|
||||
{ value: '0.75', label: '75%' },
|
||||
{ value: '0.80', label: '80%' },
|
||||
{ value: '0.90', label: '90%' },
|
||||
{ value: '0.95', label: '95%' },
|
||||
] as const;
|
||||
|
||||
// Chart Colors
|
||||
export const CHART_COLORS = [
|
||||
'#3b82f6', // blue
|
||||
'#22c55e', // green
|
||||
'#f97316', // orange
|
||||
'#8b5cf6', // purple
|
||||
'#ef4444', // red
|
||||
'#06b6d4', // cyan
|
||||
'#ec4899', // pink
|
||||
'#eab308', // yellow
|
||||
'#6366f1', // indigo
|
||||
'#14b8a6', // teal
|
||||
'#f43f5e', // rose
|
||||
'#84cc16', // lime
|
||||
];
|
||||
3
apps-archived/finance/packages/shared/src/index.ts
Normal file
3
apps-archived/finance/packages/shared/src/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './types';
|
||||
export * from './constants';
|
||||
export * from './utils';
|
||||
311
apps-archived/finance/packages/shared/src/types/index.ts
Normal file
311
apps-archived/finance/packages/shared/src/types/index.ts
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
// Account Types
|
||||
export type AccountType =
|
||||
| 'checking'
|
||||
| 'savings'
|
||||
| 'credit_card'
|
||||
| 'cash'
|
||||
| 'investment'
|
||||
| 'loan'
|
||||
| 'other';
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
type: AccountType;
|
||||
balance: string;
|
||||
currency: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
isArchived: boolean;
|
||||
includeInTotal: boolean;
|
||||
order: number;
|
||||
description?: string;
|
||||
institutionName?: string;
|
||||
accountNumber?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateAccountInput {
|
||||
name: string;
|
||||
type: AccountType;
|
||||
balance?: string;
|
||||
currency?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
institutionName?: string;
|
||||
accountNumber?: string;
|
||||
includeInTotal?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateAccountInput extends Partial<CreateAccountInput> {
|
||||
isArchived?: boolean;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
// Category Types
|
||||
export type CategoryType = 'income' | 'expense';
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
type: CategoryType;
|
||||
parentId?: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
order: number;
|
||||
isSystem: boolean;
|
||||
isArchived: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateCategoryInput {
|
||||
name: string;
|
||||
type: CategoryType;
|
||||
parentId?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCategoryInput extends Partial<CreateCategoryInput> {
|
||||
isArchived?: boolean;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
// Transaction Types
|
||||
export type TransactionType = 'income' | 'expense';
|
||||
|
||||
export interface RecurrenceRule {
|
||||
frequency: 'daily' | 'weekly' | 'biweekly' | 'monthly' | 'yearly';
|
||||
interval: number;
|
||||
endDate?: string;
|
||||
dayOfMonth?: number;
|
||||
dayOfWeek?: number;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
userId: string;
|
||||
accountId: string;
|
||||
categoryId?: string;
|
||||
type: TransactionType;
|
||||
amount: string;
|
||||
currency: string;
|
||||
date: string;
|
||||
description?: string;
|
||||
notes?: string;
|
||||
payee?: string;
|
||||
isRecurring: boolean;
|
||||
recurrenceRule?: RecurrenceRule;
|
||||
parentTransactionId?: string;
|
||||
isPending: boolean;
|
||||
isReconciled: boolean;
|
||||
tags: string[];
|
||||
attachments: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
// Joined data
|
||||
account?: Account;
|
||||
category?: Category;
|
||||
}
|
||||
|
||||
export interface CreateTransactionInput {
|
||||
accountId: string;
|
||||
categoryId?: string;
|
||||
type: TransactionType;
|
||||
amount: string;
|
||||
currency?: string;
|
||||
date: string;
|
||||
description?: string;
|
||||
notes?: string;
|
||||
payee?: string;
|
||||
isRecurring?: boolean;
|
||||
recurrenceRule?: RecurrenceRule;
|
||||
isPending?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateTransactionInput extends Partial<CreateTransactionInput> {
|
||||
isReconciled?: boolean;
|
||||
}
|
||||
|
||||
export interface TransactionFilters {
|
||||
accountId?: string;
|
||||
categoryId?: string;
|
||||
type?: TransactionType;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
minAmount?: string;
|
||||
maxAmount?: string;
|
||||
search?: string;
|
||||
isPending?: boolean;
|
||||
isRecurring?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// Budget Types
|
||||
export interface Budget {
|
||||
id: string;
|
||||
userId: string;
|
||||
categoryId?: string;
|
||||
month: number;
|
||||
year: number;
|
||||
amount: string;
|
||||
currency: string;
|
||||
alertThreshold: string;
|
||||
alertEnabled: boolean;
|
||||
rolloverEnabled: boolean;
|
||||
rolloverAmount: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
// Computed
|
||||
spent?: string;
|
||||
remaining?: string;
|
||||
percentage?: number;
|
||||
category?: Category;
|
||||
}
|
||||
|
||||
export interface CreateBudgetInput {
|
||||
categoryId?: string;
|
||||
month: number;
|
||||
year: number;
|
||||
amount: string;
|
||||
currency?: string;
|
||||
alertThreshold?: string;
|
||||
alertEnabled?: boolean;
|
||||
rolloverEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateBudgetInput extends Partial<CreateBudgetInput> {}
|
||||
|
||||
// Transfer Types
|
||||
export interface Transfer {
|
||||
id: string;
|
||||
userId: string;
|
||||
fromAccountId: string;
|
||||
toAccountId: string;
|
||||
amount: string;
|
||||
date: string;
|
||||
description?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
// Joined data
|
||||
fromAccount?: Account;
|
||||
toAccount?: Account;
|
||||
}
|
||||
|
||||
export interface CreateTransferInput {
|
||||
fromAccountId: string;
|
||||
toAccountId: string;
|
||||
amount: string;
|
||||
date: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTransferInput extends Partial<CreateTransferInput> {}
|
||||
|
||||
// Exchange Rate Types
|
||||
export interface ExchangeRate {
|
||||
id: string;
|
||||
fromCurrency: string;
|
||||
toCurrency: string;
|
||||
rate: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
// User Settings Types
|
||||
export interface UserSettings {
|
||||
id: string;
|
||||
userId: string;
|
||||
defaultCurrency: string;
|
||||
locale: string;
|
||||
dateFormat: string;
|
||||
weekStartsOn: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface UpdateUserSettingsInput {
|
||||
defaultCurrency?: string;
|
||||
locale?: string;
|
||||
dateFormat?: string;
|
||||
weekStartsOn?: number;
|
||||
}
|
||||
|
||||
// Report Types
|
||||
export interface DashboardData {
|
||||
totalBalance: number;
|
||||
totalBalanceByCurrency: Record<string, number>;
|
||||
monthlyIncome: number;
|
||||
monthlyExpenses: number;
|
||||
monthlyNet: number;
|
||||
budgetProgress: BudgetProgress[];
|
||||
recentTransactions: Transaction[];
|
||||
accountBalances: AccountBalance[];
|
||||
}
|
||||
|
||||
export interface BudgetProgress {
|
||||
categoryId: string;
|
||||
categoryName: string;
|
||||
categoryColor: string;
|
||||
budgeted: number;
|
||||
spent: number;
|
||||
remaining: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface AccountBalance {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
accountType: AccountType;
|
||||
accountColor: string;
|
||||
balance: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface MonthlySummary {
|
||||
month: number;
|
||||
year: number;
|
||||
income: number;
|
||||
expenses: number;
|
||||
net: number;
|
||||
byCategory: CategoryBreakdown[];
|
||||
}
|
||||
|
||||
export interface CategoryBreakdown {
|
||||
categoryId: string;
|
||||
categoryName: string;
|
||||
categoryColor: string;
|
||||
categoryIcon: string;
|
||||
amount: number;
|
||||
percentage: number;
|
||||
transactionCount: number;
|
||||
}
|
||||
|
||||
export interface TrendData {
|
||||
date: string;
|
||||
income: number;
|
||||
expenses: number;
|
||||
net: number;
|
||||
}
|
||||
|
||||
// Connected Account Types (Bank Sync Preparation)
|
||||
export type ConnectionStatus = 'active' | 'disconnected' | 'error';
|
||||
|
||||
export interface ConnectedAccount {
|
||||
id: string;
|
||||
userId: string;
|
||||
accountId: string;
|
||||
provider: string;
|
||||
externalId: string;
|
||||
status: ConnectionStatus;
|
||||
lastSyncAt?: Date;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
214
apps-archived/finance/packages/shared/src/utils/index.ts
Normal file
214
apps-archived/finance/packages/shared/src/utils/index.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { CURRENCIES, DEFAULT_CURRENCY } from '../constants';
|
||||
|
||||
/**
|
||||
* Format a number as currency
|
||||
*/
|
||||
export function formatCurrency(
|
||||
amount: number | string,
|
||||
currency: string = DEFAULT_CURRENCY,
|
||||
locale: string = 'de-DE'
|
||||
): string {
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
const currencyInfo = CURRENCIES.find((c) => c.code === currency);
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: currencyInfo?.decimalDigits ?? 2,
|
||||
maximumFractionDigits: currencyInfo?.decimalDigits ?? 2,
|
||||
}).format(numAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number with thousand separators
|
||||
*/
|
||||
export function formatNumber(amount: number | string, locale: string = 'de-DE'): string {
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
return new Intl.NumberFormat(locale).format(numAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a currency string to number
|
||||
*/
|
||||
export function parseCurrency(value: string): number {
|
||||
// Remove currency symbols and thousand separators
|
||||
const cleaned = value.replace(/[^0-9,.-]/g, '').replace(',', '.');
|
||||
return parseFloat(cleaned) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date string
|
||||
*/
|
||||
export function formatDate(
|
||||
date: string | Date,
|
||||
format: string = 'dd.MM.yyyy',
|
||||
locale: string = 'de-DE'
|
||||
): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
|
||||
const day = d.getDate().toString().padStart(2, '0');
|
||||
const month = (d.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = d.getFullYear().toString();
|
||||
|
||||
switch (format) {
|
||||
case 'dd.MM.yyyy':
|
||||
return `${day}.${month}.${year}`;
|
||||
case 'MM/dd/yyyy':
|
||||
return `${month}/${day}/${year}`;
|
||||
case 'yyyy-MM-dd':
|
||||
return `${year}-${month}-${day}`;
|
||||
case 'dd/MM/yyyy':
|
||||
return `${day}/${month}/${year}`;
|
||||
default:
|
||||
return d.toLocaleDateString(locale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current month and year
|
||||
*/
|
||||
export function getCurrentMonthYear(): { month: number; year: number } {
|
||||
const now = new Date();
|
||||
return {
|
||||
month: now.getMonth() + 1,
|
||||
year: now.getFullYear(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date range for a month
|
||||
*/
|
||||
export function getMonthDateRange(
|
||||
month: number,
|
||||
year: number
|
||||
): { startDate: string; endDate: string } {
|
||||
const startDate = new Date(year, month - 1, 1);
|
||||
const endDate = new Date(year, month, 0); // Last day of month
|
||||
|
||||
return {
|
||||
startDate: startDate.toISOString().split('T')[0],
|
||||
endDate: endDate.toISOString().split('T')[0],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate budget percentage
|
||||
*/
|
||||
export function calculateBudgetPercentage(spent: number, budgeted: number): number {
|
||||
if (budgeted <= 0) return 0;
|
||||
return Math.round((spent / budgeted) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get budget status based on percentage
|
||||
*/
|
||||
export function getBudgetStatus(percentage: number): 'ok' | 'warning' | 'danger' | 'over' {
|
||||
if (percentage >= 100) return 'over';
|
||||
if (percentage >= 90) return 'danger';
|
||||
if (percentage >= 75) return 'warning';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a color from a string (for consistent category colors)
|
||||
*/
|
||||
export function stringToColor(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
const hue = hash % 360;
|
||||
return `hsl(${hue}, 65%, 50%)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate net worth from accounts
|
||||
*/
|
||||
export function calculateNetWorth(
|
||||
accounts: { balance: string; type: string; includeInTotal: boolean }[]
|
||||
): number {
|
||||
return accounts
|
||||
.filter((a) => a.includeInTotal)
|
||||
.reduce((sum, account) => {
|
||||
const balance = parseFloat(account.balance);
|
||||
// Credit cards and loans are liabilities (negative)
|
||||
if (account.type === 'credit_card' || account.type === 'loan') {
|
||||
return sum - Math.abs(balance);
|
||||
}
|
||||
return sum + balance;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group transactions by date
|
||||
*/
|
||||
export function groupByDate<T extends { date: string }>(items: T[]): Record<string, T[]> {
|
||||
return items.reduce(
|
||||
(groups, item) => {
|
||||
const date = item.date;
|
||||
if (!groups[date]) {
|
||||
groups[date] = [];
|
||||
}
|
||||
groups[date].push(item);
|
||||
return groups;
|
||||
},
|
||||
{} as Record<string, T[]>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group transactions by category
|
||||
*/
|
||||
export function groupByCategory<T extends { categoryId?: string }>(
|
||||
items: T[]
|
||||
): Record<string, T[]> {
|
||||
return items.reduce(
|
||||
(groups, item) => {
|
||||
const categoryId = item.categoryId || 'uncategorized';
|
||||
if (!groups[categoryId]) {
|
||||
groups[categoryId] = [];
|
||||
}
|
||||
groups[categoryId].push(item);
|
||||
return groups;
|
||||
},
|
||||
{} as Record<string, T[]>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by date (newest first)
|
||||
*/
|
||||
export function sortByDateDesc<T extends { date: string }>(items: T[]): T[] {
|
||||
return [...items].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by date (oldest first)
|
||||
*/
|
||||
export function sortByDateAsc<T extends { date: string }>(items: T[]): T[] {
|
||||
return [...items].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate sum of amounts
|
||||
*/
|
||||
export function sumAmounts(items: { amount: string }[]): number {
|
||||
return items.reduce((sum, item) => sum + parseFloat(item.amount), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IBAN (basic check)
|
||||
*/
|
||||
export function isValidIBAN(iban: string): boolean {
|
||||
const cleaned = iban.replace(/\s/g, '').toUpperCase();
|
||||
return /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/.test(cleaned);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format IBAN with spaces
|
||||
*/
|
||||
export function formatIBAN(iban: string): string {
|
||||
const cleaned = iban.replace(/\s/g, '').toUpperCase();
|
||||
return cleaned.match(/.{1,4}/g)?.join(' ') || cleaned;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue