Enhance Cospend with debt breakdown and predefined users

- Add EnhancedBalance component with integrated single-user debt display
- Create DebtBreakdown component for multi-user debt overview
- Add predefined users configuration (alexander, anna)
- Implement personal + equal split payment method
- Add profile pictures throughout payment interfaces
- Integrate debt information with profile pictures in balance view
- Auto-hide debt breakdown when single user (shows in balance instead)
- Support both manual and predefined user management modes

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-09 18:58:04 +02:00
parent b67bb0b263
commit fd4a25376b
13 changed files with 1019 additions and 150 deletions

View File

@@ -0,0 +1,110 @@
import type { RequestHandler } from '@sveltejs/kit';
import { PaymentSplit } from '../../../../models/PaymentSplit';
import { Payment } from '../../../../models/Payment';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { error, json } from '@sveltejs/kit';
interface DebtSummary {
username: string;
netAmount: number; // positive = you owe them, negative = they owe you
transactions: {
paymentId: string;
title: string;
amount: number;
date: Date;
category: string;
}[];
}
export const GET: RequestHandler = async ({ locals }) => {
const auth = await locals.auth();
if (!auth || !auth.user?.nickname) {
throw error(401, 'Not logged in');
}
const currentUser = auth.user.nickname;
await dbConnect();
try {
// Get all splits for the current user
const userSplits = await PaymentSplit.find({ username: currentUser })
.populate('paymentId')
.lean();
// Get all other users who have splits with payments involving the current user
const paymentIds = userSplits.map(split => split.paymentId._id);
const allRelatedSplits = await PaymentSplit.find({
paymentId: { $in: paymentIds },
username: { $ne: currentUser }
})
.populate('paymentId')
.lean();
// Group debts by user
const debtsByUser = new Map<string, DebtSummary>();
// Process current user's splits to understand what they owe/are owed
for (const split of userSplits) {
const payment = split.paymentId as any;
if (!payment) continue;
// Find other participants in this payment
const otherSplits = allRelatedSplits.filter(s =>
s.paymentId._id.toString() === split.paymentId._id.toString()
);
for (const otherSplit of otherSplits) {
const otherUser = otherSplit.username;
if (!debtsByUser.has(otherUser)) {
debtsByUser.set(otherUser, {
username: otherUser,
netAmount: 0,
transactions: []
});
}
const debt = debtsByUser.get(otherUser)!;
// Current user's amount: positive = they owe, negative = they are owed
// We want to show net between the two users
debt.netAmount += split.amount;
debt.transactions.push({
paymentId: payment._id.toString(),
title: payment.title,
amount: split.amount,
date: payment.date,
category: payment.category
});
}
}
// Convert map to array and sort by absolute amount (largest debts first)
const debtSummaries = Array.from(debtsByUser.values())
.filter(debt => Math.abs(debt.netAmount) > 0.01) // Filter out tiny amounts
.sort((a, b) => Math.abs(b.netAmount) - Math.abs(a.netAmount));
// Separate into who owes you vs who you owe
const whoOwesMe = debtSummaries.filter(debt => debt.netAmount < 0).map(debt => ({
...debt,
netAmount: Math.abs(debt.netAmount) // Make positive for display
}));
const whoIOwe = debtSummaries.filter(debt => debt.netAmount > 0);
return json({
whoOwesMe,
whoIOwe,
totalOwedToMe: whoOwesMe.reduce((sum, debt) => sum + debt.netAmount, 0),
totalIOwe: whoIOwe.reduce((sum, debt) => sum + debt.netAmount, 0)
});
} catch (e) {
console.error('Error calculating debt breakdown:', e);
throw error(500, 'Failed to calculate debt breakdown');
} finally {
await dbDisconnect();
}
};

View File

@@ -48,7 +48,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
throw error(400, 'Amount must be positive');
}
if (!['equal', 'full', 'proportional'].includes(splitMethod)) {
if (!['equal', 'full', 'proportional', 'personal_equal'].includes(splitMethod)) {
throw error(400, 'Invalid split method');
}
@@ -56,6 +56,17 @@ export const POST: RequestHandler = async ({ request, locals }) => {
throw error(400, 'Invalid category');
}
// Validate personal + equal split method
if (splitMethod === 'personal_equal' && splits) {
const totalPersonal = splits.reduce((sum: number, split: any) => {
return sum + (parseFloat(split.personalAmount) || 0);
}, 0);
if (totalPersonal > amount) {
throw error(400, 'Personal amounts cannot exceed total payment amount');
}
}
await dbConnect();
try {
@@ -77,7 +88,8 @@ export const POST: RequestHandler = async ({ request, locals }) => {
paymentId: payment._id,
username: split.username,
amount: split.amount,
proportion: split.proportion
proportion: split.proportion,
personalAmount: split.personalAmount
});
});

View File

@@ -75,7 +75,8 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
paymentId: id,
username: split.username,
amount: split.amount,
proportion: split.proportion
proportion: split.proportion,
personalAmount: split.personalAmount
});
});