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:
Till-JS 2025-12-05 13:13:15 +01:00
parent c3c272abc9
commit ace7fa8f7f
427 changed files with 0 additions and 0 deletions

View 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
];

View file

@ -0,0 +1,3 @@
export * from './types';
export * from './constants';
export * from './utils';

View 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;
}

View 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;
}