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:
		
							
								
								
									
										246
									
								
								src/lib/components/DebtBreakdown.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								src/lib/components/DebtBreakdown.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,246 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
 | 
					  import ProfilePicture from './ProfilePicture.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let debtData = {
 | 
				
			||||||
 | 
					    whoOwesMe: [],
 | 
				
			||||||
 | 
					    whoIOwe: [],
 | 
				
			||||||
 | 
					    totalOwedToMe: 0,
 | 
				
			||||||
 | 
					    totalIOwe: 0
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  let loading = true;
 | 
				
			||||||
 | 
					  let error = null;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  $: shouldHide = getShouldHide();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  function getShouldHide() {
 | 
				
			||||||
 | 
					    const totalUsers = debtData.whoOwesMe.length + debtData.whoIOwe.length;
 | 
				
			||||||
 | 
					    return totalUsers <= 1; // Hide if 0 or 1 user (1 user is handled by enhanced balance)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMount(async () => {
 | 
				
			||||||
 | 
					    await fetchDebtBreakdown();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function fetchDebtBreakdown() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      loading = true;
 | 
				
			||||||
 | 
					      const response = await fetch('/api/cospend/debts');
 | 
				
			||||||
 | 
					      if (!response.ok) {
 | 
				
			||||||
 | 
					        throw new Error('Failed to fetch debt breakdown');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      debtData = await response.json();
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      error = err.message;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      loading = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function formatCurrency(amount) {
 | 
				
			||||||
 | 
					    return new Intl.NumberFormat('de-CH', {
 | 
				
			||||||
 | 
					      style: 'currency',
 | 
				
			||||||
 | 
					      currency: 'CHF'
 | 
				
			||||||
 | 
					    }).format(amount);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if !shouldHide}
 | 
				
			||||||
 | 
					<div class="debt-breakdown">
 | 
				
			||||||
 | 
					  <h2>Debt Overview</h2>
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  {#if loading}
 | 
				
			||||||
 | 
					    <div class="loading">Loading debt breakdown...</div>
 | 
				
			||||||
 | 
					  {:else if error}
 | 
				
			||||||
 | 
					    <div class="error">Error: {error}</div>
 | 
				
			||||||
 | 
					  {:else}
 | 
				
			||||||
 | 
					    <div class="debt-sections">
 | 
				
			||||||
 | 
					      {#if debtData.whoOwesMe.length > 0}
 | 
				
			||||||
 | 
					        <div class="debt-section owed-to-me">
 | 
				
			||||||
 | 
					          <h3>Who owes you</h3>
 | 
				
			||||||
 | 
					          <div class="total-amount positive">
 | 
				
			||||||
 | 
					            Total: {formatCurrency(debtData.totalOwedToMe)}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div class="debt-list">
 | 
				
			||||||
 | 
					            {#each debtData.whoOwesMe as debt}
 | 
				
			||||||
 | 
					              <div class="debt-item">
 | 
				
			||||||
 | 
					                <div class="debt-user">
 | 
				
			||||||
 | 
					                  <ProfilePicture username={debt.username} size={40} />
 | 
				
			||||||
 | 
					                  <div class="user-details">
 | 
				
			||||||
 | 
					                    <span class="username">{debt.username}</span>
 | 
				
			||||||
 | 
					                    <span class="amount positive">{formatCurrency(debt.netAmount)}</span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="transaction-count">
 | 
				
			||||||
 | 
					                  {debt.transactions.length} transaction{debt.transactions.length !== 1 ? 's' : ''}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            {/each}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {#if debtData.whoIOwe.length > 0}
 | 
				
			||||||
 | 
					        <div class="debt-section owe-to-others">
 | 
				
			||||||
 | 
					          <h3>You owe</h3>
 | 
				
			||||||
 | 
					          <div class="total-amount negative">
 | 
				
			||||||
 | 
					            Total: {formatCurrency(debtData.totalIOwe)}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          <div class="debt-list">
 | 
				
			||||||
 | 
					            {#each debtData.whoIOwe as debt}
 | 
				
			||||||
 | 
					              <div class="debt-item">
 | 
				
			||||||
 | 
					                <div class="debt-user">
 | 
				
			||||||
 | 
					                  <ProfilePicture username={debt.username} size={40} />
 | 
				
			||||||
 | 
					                  <div class="user-details">
 | 
				
			||||||
 | 
					                    <span class="username">{debt.username}</span>
 | 
				
			||||||
 | 
					                    <span class="amount negative">{formatCurrency(debt.netAmount)}</span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="transaction-count">
 | 
				
			||||||
 | 
					                  {debt.transactions.length} transaction{debt.transactions.length !== 1 ? 's' : ''}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            {/each}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  {/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					  .debt-breakdown {
 | 
				
			||||||
 | 
					    background: white;
 | 
				
			||||||
 | 
					    padding: 1.5rem;
 | 
				
			||||||
 | 
					    border-radius: 0.75rem;
 | 
				
			||||||
 | 
					    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					    margin-bottom: 2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-breakdown h2 {
 | 
				
			||||||
 | 
					    margin-bottom: 1.5rem;
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					    font-size: 1.4rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .loading, .error {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    padding: 2rem;
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error {
 | 
				
			||||||
 | 
					    color: #d32f2f;
 | 
				
			||||||
 | 
					    background-color: #ffebee;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .no-debts {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    padding: 2rem;
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-sections {
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    gap: 1.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (min-width: 768px) {
 | 
				
			||||||
 | 
					    .debt-sections {
 | 
				
			||||||
 | 
					      grid-template-columns: 1fr 1fr;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-section {
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-section.owed-to-me {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #e8f5e8, #f0f8f0);
 | 
				
			||||||
 | 
					    border: 1px solid #c8e6c9;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-section.owe-to-others {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #ffeaea, #fff5f5);
 | 
				
			||||||
 | 
					    border: 1px solid #ffcdd2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-section h3 {
 | 
				
			||||||
 | 
					    margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .total-amount {
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    font-size: 1.2rem;
 | 
				
			||||||
 | 
					    margin-bottom: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .total-amount.positive {
 | 
				
			||||||
 | 
					    color: #2e7d32;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .total-amount.negative {
 | 
				
			||||||
 | 
					    color: #d32f2f;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-list {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-item {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    padding: 0.75rem;
 | 
				
			||||||
 | 
					    background: white;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-user {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .user-details {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 0.25rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .username {
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .amount {
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .amount.positive {
 | 
				
			||||||
 | 
					    color: #2e7d32;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .amount.negative {
 | 
				
			||||||
 | 
					    color: #d32f2f;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .transaction-count {
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-size: 0.85rem;
 | 
				
			||||||
 | 
					    text-align: right;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										334
									
								
								src/lib/components/EnhancedBalance.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										334
									
								
								src/lib/components/EnhancedBalance.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,334 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
 | 
					  import ProfilePicture from './ProfilePicture.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let balance = {
 | 
				
			||||||
 | 
					    netBalance: 0,
 | 
				
			||||||
 | 
					    recentSplits: []
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  let debtData = {
 | 
				
			||||||
 | 
					    whoOwesMe: [],
 | 
				
			||||||
 | 
					    whoIOwe: [],
 | 
				
			||||||
 | 
					    totalOwedToMe: 0,
 | 
				
			||||||
 | 
					    totalIOwe: 0
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  let loading = true;
 | 
				
			||||||
 | 
					  let error = null;
 | 
				
			||||||
 | 
					  let singleDebtUser = null;
 | 
				
			||||||
 | 
					  let shouldShowIntegratedView = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getSingleDebtUser() {
 | 
				
			||||||
 | 
					    const totalUsers = debtData.whoOwesMe.length + debtData.whoIOwe.length;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (totalUsers === 1) {
 | 
				
			||||||
 | 
					      if (debtData.whoOwesMe.length === 1) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          type: 'owesMe',
 | 
				
			||||||
 | 
					          user: debtData.whoOwesMe[0],
 | 
				
			||||||
 | 
					          amount: debtData.whoOwesMe[0].netAmount
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      } else if (debtData.whoIOwe.length === 1) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          type: 'iOwe', 
 | 
				
			||||||
 | 
					          user: debtData.whoIOwe[0],
 | 
				
			||||||
 | 
					          amount: debtData.whoIOwe[0].netAmount
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: {
 | 
				
			||||||
 | 
					    // Recalculate when debtData changes
 | 
				
			||||||
 | 
					    singleDebtUser = getSingleDebtUser();
 | 
				
			||||||
 | 
					    shouldShowIntegratedView = singleDebtUser !== null;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Temporary debug logging
 | 
				
			||||||
 | 
					    if (!loading) {
 | 
				
			||||||
 | 
					      console.log('🔍 Debug Info:');
 | 
				
			||||||
 | 
					      console.log('- debtData:', debtData);
 | 
				
			||||||
 | 
					      console.log('- whoOwesMe length:', debtData.whoOwesMe.length);
 | 
				
			||||||
 | 
					      console.log('- whoIOwe length:', debtData.whoIOwe.length);
 | 
				
			||||||
 | 
					      console.log('- singleDebtUser:', singleDebtUser);
 | 
				
			||||||
 | 
					      console.log('- shouldShowIntegratedView:', shouldShowIntegratedView);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMount(async () => {
 | 
				
			||||||
 | 
					    await Promise.all([fetchBalance(), fetchDebtBreakdown()]);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function fetchBalance() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await fetch('/api/cospend/balance');
 | 
				
			||||||
 | 
					      if (!response.ok) {
 | 
				
			||||||
 | 
					        throw new Error('Failed to fetch balance');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      balance = await response.json();
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      error = err.message;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function fetchDebtBreakdown() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const response = await fetch('/api/cospend/debts');
 | 
				
			||||||
 | 
					      if (!response.ok) {
 | 
				
			||||||
 | 
					        throw new Error('Failed to fetch debt breakdown');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      debtData = await response.json();
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      error = err.message;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      loading = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function formatCurrency(amount) {
 | 
				
			||||||
 | 
					    return new Intl.NumberFormat('de-CH', {
 | 
				
			||||||
 | 
					      style: 'currency',
 | 
				
			||||||
 | 
					      currency: 'CHF'
 | 
				
			||||||
 | 
					    }).format(Math.abs(amount));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="balance-cards">
 | 
				
			||||||
 | 
					  <div class="balance-card net-balance" 
 | 
				
			||||||
 | 
					       class:positive={balance.netBalance <= 0} 
 | 
				
			||||||
 | 
					       class:negative={balance.netBalance > 0}
 | 
				
			||||||
 | 
					       class:enhanced={shouldShowIntegratedView}>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    {#if loading}
 | 
				
			||||||
 | 
					      <div class="loading-content">
 | 
				
			||||||
 | 
					        <h3>Your Balance</h3>
 | 
				
			||||||
 | 
					        <div class="loading">Loading...</div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    {:else if error}
 | 
				
			||||||
 | 
					      <h3>Your Balance</h3>
 | 
				
			||||||
 | 
					      <div class="error">Error: {error}</div>
 | 
				
			||||||
 | 
					    {:else if shouldShowIntegratedView}
 | 
				
			||||||
 | 
					      <!-- Enhanced view with single user debt -->
 | 
				
			||||||
 | 
					      <h3>Your Balance</h3>
 | 
				
			||||||
 | 
					      <div class="enhanced-balance">
 | 
				
			||||||
 | 
					        <div class="main-amount">
 | 
				
			||||||
 | 
					          {#if balance.netBalance < 0}
 | 
				
			||||||
 | 
					            <span class="positive">+{formatCurrency(balance.netBalance)}</span>
 | 
				
			||||||
 | 
					            <small>You are owed</small>
 | 
				
			||||||
 | 
					          {:else if balance.netBalance > 0}
 | 
				
			||||||
 | 
					            <span class="negative">-{formatCurrency(balance.netBalance)}</span>
 | 
				
			||||||
 | 
					            <small>You owe</small>
 | 
				
			||||||
 | 
					          {:else}
 | 
				
			||||||
 | 
					            <span class="even">CHF 0.00</span>
 | 
				
			||||||
 | 
					            <small>You're all even</small>
 | 
				
			||||||
 | 
					          {/if}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="debt-details">
 | 
				
			||||||
 | 
					          <div class="debt-user">
 | 
				
			||||||
 | 
					            {#if singleDebtUser && singleDebtUser.user}
 | 
				
			||||||
 | 
					              <!-- Debug: ProfilePicture with username: {singleDebtUser.user.username} -->
 | 
				
			||||||
 | 
					              <ProfilePicture username={singleDebtUser.user.username} size={40} />
 | 
				
			||||||
 | 
					              <div class="user-info">
 | 
				
			||||||
 | 
					                <span class="username">{singleDebtUser.user.username}</span>
 | 
				
			||||||
 | 
					                <span class="debt-description">
 | 
				
			||||||
 | 
					                  {#if singleDebtUser.type === 'owesMe'}
 | 
				
			||||||
 | 
					                    owes you {formatCurrency(singleDebtUser.amount)}
 | 
				
			||||||
 | 
					                  {:else}
 | 
				
			||||||
 | 
					                    you owe {formatCurrency(singleDebtUser.amount)}
 | 
				
			||||||
 | 
					                  {/if}
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            {:else}
 | 
				
			||||||
 | 
					              <div>Debug: No singleDebtUser data</div>
 | 
				
			||||||
 | 
					            {/if}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="transaction-count">
 | 
				
			||||||
 | 
					            {#if singleDebtUser && singleDebtUser.user && singleDebtUser.user.transactions}
 | 
				
			||||||
 | 
					              {singleDebtUser.user.transactions.length} transaction{singleDebtUser.user.transactions.length !== 1 ? 's' : ''}
 | 
				
			||||||
 | 
					            {/if}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    {:else}
 | 
				
			||||||
 | 
					      <!-- Standard balance view -->
 | 
				
			||||||
 | 
					      <h3>Your Balance</h3>
 | 
				
			||||||
 | 
					      <div class="amount">
 | 
				
			||||||
 | 
					        {#if balance.netBalance < 0}
 | 
				
			||||||
 | 
					          <span class="positive">+{formatCurrency(balance.netBalance)}</span>
 | 
				
			||||||
 | 
					          <small>You are owed</small>
 | 
				
			||||||
 | 
					        {:else if balance.netBalance > 0}
 | 
				
			||||||
 | 
					          <span class="negative">-{formatCurrency(balance.netBalance)}</span>
 | 
				
			||||||
 | 
					          <small>You owe</small>
 | 
				
			||||||
 | 
					        {:else}
 | 
				
			||||||
 | 
					          <span class="even">CHF 0.00</span>
 | 
				
			||||||
 | 
					          <small>You're all even</small>
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					  .balance-cards {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    margin-bottom: 2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .balance-card {
 | 
				
			||||||
 | 
					    background: white;
 | 
				
			||||||
 | 
					    padding: 2rem;
 | 
				
			||||||
 | 
					    border-radius: 0.75rem;
 | 
				
			||||||
 | 
					    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    min-width: 300px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .balance-card.enhanced {
 | 
				
			||||||
 | 
					    min-width: 400px;
 | 
				
			||||||
 | 
					    text-align: left;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .balance-card.net-balance {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .balance-card.net-balance.positive {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #e8f5e8, #d4edda);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .balance-card.net-balance.negative {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #ffeaea, #f8d7da);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .balance-card h3 {
 | 
				
			||||||
 | 
					    margin-bottom: 1rem;
 | 
				
			||||||
 | 
					    color: #555;
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .loading-content {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .loading {
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error {
 | 
				
			||||||
 | 
					    color: #d32f2f;
 | 
				
			||||||
 | 
					    background-color: #ffebee;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .amount {
 | 
				
			||||||
 | 
					    font-size: 2rem;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .amount small {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    font-weight: normal;
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    margin-top: 0.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .enhanced-balance {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 1.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .main-amount {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    font-size: 1.8rem;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .main-amount small {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    font-weight: normal;
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    margin-top: 0.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-details {
 | 
				
			||||||
 | 
					    background: rgba(255, 255, 255, 0.5);
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-user {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					    flex: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .user-info {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 0.25rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .username {
 | 
				
			||||||
 | 
					    font-weight: 600;
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .debt-description {
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .transaction-count {
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-size: 0.85rem;
 | 
				
			||||||
 | 
					    text-align: right;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .positive {
 | 
				
			||||||
 | 
					    color: #2e7d32;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .negative {
 | 
				
			||||||
 | 
					    color: #d32f2f;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .even {
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: 600px) {
 | 
				
			||||||
 | 
					    .balance-card {
 | 
				
			||||||
 | 
					      min-width: unset;
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    .balance-card.enhanced {
 | 
				
			||||||
 | 
					      min-width: unset;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .debt-details {
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 0.75rem;
 | 
				
			||||||
 | 
					      align-items: flex-start;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .transaction-count {
 | 
				
			||||||
 | 
					      text-align: left;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -79,6 +79,8 @@
 | 
				
			|||||||
      return `Split equally among ${payment.splits.length} people`;
 | 
					      return `Split equally among ${payment.splits.length} people`;
 | 
				
			||||||
    } else if (payment.splitMethod === 'full') {
 | 
					    } else if (payment.splitMethod === 'full') {
 | 
				
			||||||
      return `Paid in full by ${payment.paidBy}`;
 | 
					      return `Paid in full by ${payment.paidBy}`;
 | 
				
			||||||
 | 
					    } else if (payment.splitMethod === 'personal_equal') {
 | 
				
			||||||
 | 
					      return `Personal amounts + equal split among ${payment.splits.length} people`;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return `Custom split among ${payment.splits.length} people`;
 | 
					      return `Custom split among ${payment.splits.length} people`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										16
									
								
								src/lib/config/users.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/lib/config/users.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					// Predefined users configuration for Cospend
 | 
				
			||||||
 | 
					// When this array has exactly 2 users, the system will always split between them
 | 
				
			||||||
 | 
					// For more users, manual selection is allowed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const PREDEFINED_USERS = [
 | 
				
			||||||
 | 
					  'alexander',
 | 
				
			||||||
 | 
					  'anna'
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isPredefinedUsersMode(): boolean {
 | 
				
			||||||
 | 
					  return PREDEFINED_USERS.length === 2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getAvailableUsers(): string[] {
 | 
				
			||||||
 | 
					  return [...PREDEFINED_USERS];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -10,7 +10,7 @@ export interface IPayment {
 | 
				
			|||||||
  date: Date;
 | 
					  date: Date;
 | 
				
			||||||
  image?: string; // path to uploaded image
 | 
					  image?: string; // path to uploaded image
 | 
				
			||||||
  category: 'groceries' | 'shopping' | 'travel' | 'restaurant' | 'utilities' | 'fun';
 | 
					  category: 'groceries' | 'shopping' | 'travel' | 'restaurant' | 'utilities' | 'fun';
 | 
				
			||||||
  splitMethod: 'equal' | 'full' | 'proportional';
 | 
					  splitMethod: 'equal' | 'full' | 'proportional' | 'personal_equal';
 | 
				
			||||||
  createdBy: string; // username/nickname of the person who created the payment
 | 
					  createdBy: string; // username/nickname of the person who created the payment
 | 
				
			||||||
  createdAt?: Date;
 | 
					  createdAt?: Date;
 | 
				
			||||||
  updatedAt?: Date;
 | 
					  updatedAt?: Date;
 | 
				
			||||||
@@ -61,7 +61,7 @@ const PaymentSchema = new mongoose.Schema(
 | 
				
			|||||||
    splitMethod: {
 | 
					    splitMethod: {
 | 
				
			||||||
      type: String,
 | 
					      type: String,
 | 
				
			||||||
      required: true,
 | 
					      required: true,
 | 
				
			||||||
      enum: ['equal', 'full', 'proportional'],
 | 
					      enum: ['equal', 'full', 'proportional', 'personal_equal'],
 | 
				
			||||||
      default: 'equal'
 | 
					      default: 'equal'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    createdBy: { 
 | 
					    createdBy: { 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ export interface IPaymentSplit {
 | 
				
			|||||||
  username: string; // username/nickname of the person who owes/is owed
 | 
					  username: string; // username/nickname of the person who owes/is owed
 | 
				
			||||||
  amount: number; // amount this person owes (positive) or is owed (negative)
 | 
					  amount: number; // amount this person owes (positive) or is owed (negative)
 | 
				
			||||||
  proportion?: number; // for proportional splits, the proportion (e.g., 0.5 for 50%)
 | 
					  proportion?: number; // for proportional splits, the proportion (e.g., 0.5 for 50%)
 | 
				
			||||||
 | 
					  personalAmount?: number; // for personal_equal splits, the personal portion for this user
 | 
				
			||||||
  settled: boolean; // whether this split has been settled
 | 
					  settled: boolean; // whether this split has been settled
 | 
				
			||||||
  settledAt?: Date;
 | 
					  settledAt?: Date;
 | 
				
			||||||
  createdAt?: Date;
 | 
					  createdAt?: Date;
 | 
				
			||||||
@@ -33,6 +34,10 @@ const PaymentSplitSchema = new mongoose.Schema(
 | 
				
			|||||||
      min: 0,
 | 
					      min: 0,
 | 
				
			||||||
      max: 1 
 | 
					      max: 1 
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    personalAmount: {
 | 
				
			||||||
 | 
					      type: Number,
 | 
				
			||||||
 | 
					      min: 0
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    settled: { 
 | 
					    settled: { 
 | 
				
			||||||
      type: Boolean, 
 | 
					      type: Boolean, 
 | 
				
			||||||
      default: false 
 | 
					      default: false 
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										110
									
								
								src/routes/api/cospend/debts/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/routes/api/cospend/debts/+server.ts
									
									
									
									
									
										Normal 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();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -48,7 +48,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
 | 
				
			|||||||
    throw error(400, 'Amount must be positive');
 | 
					    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');
 | 
					    throw error(400, 'Invalid split method');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,6 +56,17 @@ export const POST: RequestHandler = async ({ request, locals }) => {
 | 
				
			|||||||
    throw error(400, 'Invalid category');
 | 
					    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();
 | 
					  await dbConnect();
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
@@ -77,7 +88,8 @@ export const POST: RequestHandler = async ({ request, locals }) => {
 | 
				
			|||||||
        paymentId: payment._id,
 | 
					        paymentId: payment._id,
 | 
				
			||||||
        username: split.username,
 | 
					        username: split.username,
 | 
				
			||||||
        amount: split.amount,
 | 
					        amount: split.amount,
 | 
				
			||||||
        proportion: split.proportion
 | 
					        proportion: split.proportion,
 | 
				
			||||||
 | 
					        personalAmount: split.personalAmount
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -75,7 +75,8 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => {
 | 
				
			|||||||
          paymentId: id,
 | 
					          paymentId: id,
 | 
				
			||||||
          username: split.username,
 | 
					          username: split.username,
 | 
				
			||||||
          amount: split.amount,
 | 
					          amount: split.amount,
 | 
				
			||||||
          proportion: split.proportion
 | 
					          proportion: split.proportion,
 | 
				
			||||||
 | 
					          personalAmount: split.personalAmount
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@
 | 
				
			|||||||
  import { page } from '$app/stores';
 | 
					  import { page } from '$app/stores';
 | 
				
			||||||
  import { pushState } from '$app/navigation';
 | 
					  import { pushState } from '$app/navigation';
 | 
				
			||||||
  import ProfilePicture from '$lib/components/ProfilePicture.svelte';
 | 
					  import ProfilePicture from '$lib/components/ProfilePicture.svelte';
 | 
				
			||||||
 | 
					  import EnhancedBalance from '$lib/components/EnhancedBalance.svelte';
 | 
				
			||||||
 | 
					  import DebtBreakdown from '$lib/components/DebtBreakdown.svelte';
 | 
				
			||||||
  import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
 | 
					  import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let data; // Used by the layout for session data
 | 
					  export let data; // Used by the layout for session data
 | 
				
			||||||
@@ -67,83 +69,67 @@
 | 
				
			|||||||
    <p>Track and split expenses with your friends and family</p>
 | 
					    <p>Track and split expenses with your friends and family</p>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <EnhancedBalance />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="actions">
 | 
				
			||||||
 | 
					    <a href="/cospend/payments/add" class="btn btn-primary">Add Payment</a>
 | 
				
			||||||
 | 
					    <a href="/cospend/payments" class="btn btn-secondary">View All Payments</a>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <DebtBreakdown />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {#if loading}
 | 
					  {#if loading}
 | 
				
			||||||
    <div class="loading">Loading your balance...</div>
 | 
					    <div class="loading">Loading recent activity...</div>
 | 
				
			||||||
  {:else if error}
 | 
					  {:else if error}
 | 
				
			||||||
    <div class="error">Error: {error}</div>
 | 
					    <div class="error">Error: {error}</div>
 | 
				
			||||||
  {:else}
 | 
					  {:else if balance.recentSplits && balance.recentSplits.length > 0}
 | 
				
			||||||
    <div class="balance-cards">
 | 
					    <div class="recent-activity">
 | 
				
			||||||
      <div class="balance-card net-balance" class:positive={balance.netBalance <= 0} class:negative={balance.netBalance > 0}>
 | 
					      <h2>Recent Activity</h2>
 | 
				
			||||||
        <h3>Your Balance</h3>
 | 
					      <div class="activity-dialog">
 | 
				
			||||||
        <div class="amount">
 | 
					        {#each balance.recentSplits as split}
 | 
				
			||||||
          {#if balance.netBalance < 0}
 | 
					          <div class="activity-message" class:is-me={split.paymentId?.paidBy === data.session?.user?.nickname}>
 | 
				
			||||||
            <span class="positive">+{formatCurrency(balance.netBalance)}</span>
 | 
					            <div class="message-content">
 | 
				
			||||||
            <small>You are owed</small>
 | 
					              <ProfilePicture username={split.paymentId?.paidBy || 'Unknown'} size={36} />
 | 
				
			||||||
          {:else if balance.netBalance > 0}
 | 
					              <a 
 | 
				
			||||||
            <span class="negative">-{formatCurrency(balance.netBalance)}</span>
 | 
					                href="/cospend/payments/view/{split.paymentId?._id}" 
 | 
				
			||||||
            <small>You owe</small>
 | 
					                class="activity-bubble"
 | 
				
			||||||
          {:else}
 | 
					                on:click={(e) => handlePaymentClick(split.paymentId?._id, e)}
 | 
				
			||||||
            <span class="even">CHF 0.00</span>
 | 
					              >
 | 
				
			||||||
            <small>You're all even</small>
 | 
					                <div class="activity-header">
 | 
				
			||||||
          {/if}
 | 
					                  <div class="user-info">
 | 
				
			||||||
        </div>
 | 
					                    <div class="payment-title-row">
 | 
				
			||||||
      </div>
 | 
					                      <span class="category-emoji">{getCategoryEmoji(split.paymentId?.category || 'groceries')}</span>
 | 
				
			||||||
    </div>
 | 
					                      <strong class="payment-title">{split.paymentId?.title || 'Payment'}</strong>
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="actions">
 | 
					 | 
				
			||||||
      <a href="/cospend/payments/add" class="btn btn-primary">Add Payment</a>
 | 
					 | 
				
			||||||
      <a href="/cospend/payments" class="btn btn-secondary">View All Payments</a>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {#if balance.recentSplits && balance.recentSplits.length > 0}
 | 
					 | 
				
			||||||
      <div class="recent-activity">
 | 
					 | 
				
			||||||
        <h2>Recent Activity</h2>
 | 
					 | 
				
			||||||
        <div class="activity-dialog">
 | 
					 | 
				
			||||||
          {#each balance.recentSplits as split}
 | 
					 | 
				
			||||||
            <div class="activity-message" class:is-me={split.paymentId?.paidBy === data.session?.user?.nickname}>
 | 
					 | 
				
			||||||
              <div class="message-content">
 | 
					 | 
				
			||||||
                <ProfilePicture username={split.paymentId?.paidBy || 'Unknown'} size={36} />
 | 
					 | 
				
			||||||
                <a 
 | 
					 | 
				
			||||||
                  href="/cospend/payments/view/{split.paymentId?._id}" 
 | 
					 | 
				
			||||||
                  class="activity-bubble"
 | 
					 | 
				
			||||||
                  on:click={(e) => handlePaymentClick(split.paymentId?._id, e)}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <div class="activity-header">
 | 
					 | 
				
			||||||
                    <div class="user-info">
 | 
					 | 
				
			||||||
                      <div class="payment-title-row">
 | 
					 | 
				
			||||||
                        <span class="category-emoji">{getCategoryEmoji(split.paymentId?.category || 'groceries')}</span>
 | 
					 | 
				
			||||||
                        <strong class="payment-title">{split.paymentId?.title || 'Payment'}</strong>
 | 
					 | 
				
			||||||
                      </div>
 | 
					 | 
				
			||||||
                      <span class="username">Paid by {split.paymentId?.paidBy || 'Unknown'}</span>
 | 
					 | 
				
			||||||
                      <span class="category-name">{getCategoryName(split.paymentId?.category || 'groceries')}</span>
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                    <div class="activity-amount" class:positive={split.amount < 0} class:negative={split.amount > 0}>
 | 
					 | 
				
			||||||
                      {#if split.amount > 0}
 | 
					 | 
				
			||||||
                        -{formatCurrency(split.amount)}
 | 
					 | 
				
			||||||
                      {:else if split.amount < 0}
 | 
					 | 
				
			||||||
                        +{formatCurrency(split.amount)}
 | 
					 | 
				
			||||||
                      {:else}
 | 
					 | 
				
			||||||
                        even
 | 
					 | 
				
			||||||
                      {/if}
 | 
					 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <span class="username">Paid by {split.paymentId?.paidBy || 'Unknown'}</span>
 | 
				
			||||||
 | 
					                    <span class="category-name">{getCategoryName(split.paymentId?.category || 'groceries')}</span>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  <div class="payment-details">
 | 
					                  <div class="activity-amount" class:positive={split.amount < 0} class:negative={split.amount > 0}>
 | 
				
			||||||
                    <div class="payment-meta">
 | 
					                    {#if split.amount > 0}
 | 
				
			||||||
                      <span class="payment-date">{formatDate(split.createdAt)}</span>
 | 
					                      -{formatCurrency(split.amount)}
 | 
				
			||||||
                    </div>
 | 
					                    {:else if split.amount < 0}
 | 
				
			||||||
                    {#if split.paymentId?.description}
 | 
					                      +{formatCurrency(split.amount)}
 | 
				
			||||||
                      <div class="payment-description">
 | 
					                    {:else}
 | 
				
			||||||
                        {truncateDescription(split.paymentId.description)}
 | 
					                      even
 | 
				
			||||||
                      </div>
 | 
					 | 
				
			||||||
                    {/if}
 | 
					                    {/if}
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                </a>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					                <div class="payment-details">
 | 
				
			||||||
 | 
					                  <div class="payment-meta">
 | 
				
			||||||
 | 
					                    <span class="payment-date">{formatDate(split.createdAt)}</span>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {#if split.paymentId?.description}
 | 
				
			||||||
 | 
					                    <div class="payment-description">
 | 
				
			||||||
 | 
					                      {truncateDescription(split.paymentId.description)}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  {/if}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          {/each}
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        {/each}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    {/if}
 | 
					    </div>
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
</main>
 | 
					</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -182,52 +168,6 @@
 | 
				
			|||||||
    border-radius: 0.5rem;
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .balance-cards {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    justify-content: center;
 | 
					 | 
				
			||||||
    margin-bottom: 2rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .balance-card {
 | 
					 | 
				
			||||||
    background: white;
 | 
					 | 
				
			||||||
    padding: 2rem;
 | 
					 | 
				
			||||||
    border-radius: 0.75rem;
 | 
					 | 
				
			||||||
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
    min-width: 300px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .balance-card.net-balance {
 | 
					 | 
				
			||||||
    background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .balance-card.net-balance.positive {
 | 
					 | 
				
			||||||
    background: linear-gradient(135deg, #e8f5e8, #d4edda);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .balance-card.net-balance.negative {
 | 
					 | 
				
			||||||
    background: linear-gradient(135deg, #ffeaea, #f8d7da);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .balance-card h3 {
 | 
					 | 
				
			||||||
    margin-bottom: 1rem;
 | 
					 | 
				
			||||||
    color: #555;
 | 
					 | 
				
			||||||
    font-size: 1.1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .amount {
 | 
					 | 
				
			||||||
    font-size: 2rem;
 | 
					 | 
				
			||||||
    font-weight: bold;
 | 
					 | 
				
			||||||
    margin-bottom: 0.5rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .amount small {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
    font-size: 0.9rem;
 | 
					 | 
				
			||||||
    font-weight: normal;
 | 
					 | 
				
			||||||
    color: #666;
 | 
					 | 
				
			||||||
    margin-top: 0.5rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .positive {
 | 
					  .positive {
 | 
				
			||||||
    color: #2e7d32;
 | 
					    color: #2e7d32;
 | 
				
			||||||
@@ -449,11 +389,6 @@
 | 
				
			|||||||
      padding: 1rem;
 | 
					      padding: 1rem;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .balance-card {
 | 
					 | 
				
			||||||
      min-width: unset;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .actions {
 | 
					    .actions {
 | 
				
			||||||
      flex-direction: column;
 | 
					      flex-direction: column;
 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -93,6 +93,8 @@
 | 
				
			|||||||
      return `Split equally among ${payment.splits.length} people`;
 | 
					      return `Split equally among ${payment.splits.length} people`;
 | 
				
			||||||
    } else if (payment.splitMethod === 'full') {
 | 
					    } else if (payment.splitMethod === 'full') {
 | 
				
			||||||
      return `Paid in full by ${payment.paidBy}`;
 | 
					      return `Paid in full by ${payment.paidBy}`;
 | 
				
			||||||
 | 
					    } else if (payment.splitMethod === 'personal_equal') {
 | 
				
			||||||
 | 
					      return `Personal amounts + equal split among ${payment.splits.length} people`;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return `Custom split among ${payment.splits.length} people`;
 | 
					      return `Custom split among ${payment.splits.length} people`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,8 @@
 | 
				
			|||||||
  import { onMount } from 'svelte';
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					  import { goto } from '$app/navigation';
 | 
				
			||||||
  import { getCategoryOptions } from '$lib/utils/categories';
 | 
					  import { getCategoryOptions } from '$lib/utils/categories';
 | 
				
			||||||
 | 
					  import { PREDEFINED_USERS, isPredefinedUsersMode } from '$lib/config/users';
 | 
				
			||||||
 | 
					  import ProfilePicture from '$lib/components/ProfilePicture.svelte';
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  export let data;
 | 
					  export let data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,17 +20,34 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  let imageFile = null;
 | 
					  let imageFile = null;
 | 
				
			||||||
  let imagePreview = '';
 | 
					  let imagePreview = '';
 | 
				
			||||||
  let users = [data.session?.user?.nickname || ''];
 | 
					  let users = [];
 | 
				
			||||||
  let newUser = '';
 | 
					  let newUser = '';
 | 
				
			||||||
  let splitAmounts = {};
 | 
					  let splitAmounts = {};
 | 
				
			||||||
 | 
					  let personalAmounts = {};
 | 
				
			||||||
  let loading = false;
 | 
					  let loading = false;
 | 
				
			||||||
  let error = null;
 | 
					  let error = null;
 | 
				
			||||||
 | 
					  let personalTotalError = false;
 | 
				
			||||||
 | 
					  let predefinedMode = isPredefinedUsersMode();
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  $: categoryOptions = getCategoryOptions();
 | 
					  $: categoryOptions = getCategoryOptions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onMount(() => {
 | 
					  onMount(() => {
 | 
				
			||||||
    if (data.session?.user?.nickname) {
 | 
					    if (predefinedMode) {
 | 
				
			||||||
      addSplitForUser(data.session.user.nickname);
 | 
					      // Use predefined users and always split between them
 | 
				
			||||||
 | 
					      users = [...PREDEFINED_USERS];
 | 
				
			||||||
 | 
					      users.forEach(user => addSplitForUser(user));
 | 
				
			||||||
 | 
					      // Default to current user as payer if they're in the predefined list
 | 
				
			||||||
 | 
					      if (data.session?.user?.nickname && PREDEFINED_USERS.includes(data.session.user.nickname)) {
 | 
				
			||||||
 | 
					        formData.paidBy = data.session.user.nickname;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        formData.paidBy = PREDEFINED_USERS[0];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Original behavior for manual user management
 | 
				
			||||||
 | 
					      if (data.session?.user?.nickname) {
 | 
				
			||||||
 | 
					        users = [data.session.user.nickname];
 | 
				
			||||||
 | 
					        addSplitForUser(data.session.user.nickname);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -61,6 +80,8 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function addUser() {
 | 
					  function addUser() {
 | 
				
			||||||
 | 
					    if (predefinedMode) return; // No adding users in predefined mode
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    if (newUser.trim() && !users.includes(newUser.trim())) {
 | 
					    if (newUser.trim() && !users.includes(newUser.trim())) {
 | 
				
			||||||
      users = [...users, newUser.trim()];
 | 
					      users = [...users, newUser.trim()];
 | 
				
			||||||
      addSplitForUser(newUser.trim());
 | 
					      addSplitForUser(newUser.trim());
 | 
				
			||||||
@@ -69,6 +90,8 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function removeUser(userToRemove) {
 | 
					  function removeUser(userToRemove) {
 | 
				
			||||||
 | 
					    if (predefinedMode) return; // No removing users in predefined mode
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    if (users.length > 1 && userToRemove !== data.session.user.nickname) {
 | 
					    if (users.length > 1 && userToRemove !== data.session.user.nickname) {
 | 
				
			||||||
      users = users.filter(u => u !== userToRemove);
 | 
					      users = users.filter(u => u !== userToRemove);
 | 
				
			||||||
      delete splitAmounts[userToRemove];
 | 
					      delete splitAmounts[userToRemove];
 | 
				
			||||||
@@ -114,11 +137,42 @@
 | 
				
			|||||||
    splitAmounts = { ...splitAmounts };
 | 
					    splitAmounts = { ...splitAmounts };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function calculatePersonalEqualSplit() {
 | 
				
			||||||
 | 
					    if (!formData.amount || users.length === 0) return;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const totalAmount = parseFloat(formData.amount);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Calculate total personal amounts
 | 
				
			||||||
 | 
					    const totalPersonal = users.reduce((sum, user) => {
 | 
				
			||||||
 | 
					      return sum + (parseFloat(personalAmounts[user]) || 0);
 | 
				
			||||||
 | 
					    }, 0);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Remaining amount to be split equally
 | 
				
			||||||
 | 
					    const remainder = Math.max(0, totalAmount - totalPersonal);
 | 
				
			||||||
 | 
					    const equalShare = remainder / users.length;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    users.forEach(user => {
 | 
				
			||||||
 | 
					      const personalAmount = parseFloat(personalAmounts[user]) || 0;
 | 
				
			||||||
 | 
					      const totalOwed = personalAmount + equalShare;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (user === formData.paidBy) {
 | 
				
			||||||
 | 
					        // Person who paid gets back what others owe minus what they personally used
 | 
				
			||||||
 | 
					        splitAmounts[user] = totalOwed - totalAmount;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Others owe their personal amount + equal share
 | 
				
			||||||
 | 
					        splitAmounts[user] = totalOwed;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    splitAmounts = { ...splitAmounts };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function handleSplitMethodChange() {
 | 
					  function handleSplitMethodChange() {
 | 
				
			||||||
    if (formData.splitMethod === 'equal') {
 | 
					    if (formData.splitMethod === 'equal') {
 | 
				
			||||||
      calculateEqualSplits();
 | 
					      calculateEqualSplits();
 | 
				
			||||||
    } else if (formData.splitMethod === 'full') {
 | 
					    } else if (formData.splitMethod === 'full') {
 | 
				
			||||||
      calculateFullPayment();
 | 
					      calculateFullPayment();
 | 
				
			||||||
 | 
					    } else if (formData.splitMethod === 'personal_equal') {
 | 
				
			||||||
 | 
					      calculatePersonalEqualSplit();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -152,6 +206,17 @@
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Validate personal amounts for personal_equal split
 | 
				
			||||||
 | 
					    if (formData.splitMethod === 'personal_equal') {
 | 
				
			||||||
 | 
					      const totalPersonal = Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0);
 | 
				
			||||||
 | 
					      const totalAmount = parseFloat(formData.amount);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (totalPersonal > totalAmount) {
 | 
				
			||||||
 | 
					        error = 'Personal amounts cannot exceed the total payment amount';
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (users.length === 0) {
 | 
					    if (users.length === 0) {
 | 
				
			||||||
      error = 'Please add at least one user to split with';
 | 
					      error = 'Please add at least one user to split with';
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
@@ -169,7 +234,8 @@
 | 
				
			|||||||
      const splits = users.map(user => ({
 | 
					      const splits = users.map(user => ({
 | 
				
			||||||
        username: user,
 | 
					        username: user,
 | 
				
			||||||
        amount: splitAmounts[user] || 0,
 | 
					        amount: splitAmounts[user] || 0,
 | 
				
			||||||
        proportion: formData.splitMethod === 'proportional' ? (splitAmounts[user] || 0) / parseFloat(formData.amount) : undefined
 | 
					        proportion: formData.splitMethod === 'proportional' ? (splitAmounts[user] || 0) / parseFloat(formData.amount) : undefined,
 | 
				
			||||||
 | 
					        personalAmount: formData.splitMethod === 'personal_equal' ? (parseFloat(personalAmounts[user]) || 0) : undefined
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const payload = {
 | 
					      const payload = {
 | 
				
			||||||
@@ -205,6 +271,17 @@
 | 
				
			|||||||
  $: if (formData.amount && formData.splitMethod && formData.paidBy) {
 | 
					  $: if (formData.amount && formData.splitMethod && formData.paidBy) {
 | 
				
			||||||
    handleSplitMethodChange();
 | 
					    handleSplitMethodChange();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Validate and recalculate when personal amounts change
 | 
				
			||||||
 | 
					  $: if (formData.splitMethod === 'personal_equal' && personalAmounts && formData.amount) {
 | 
				
			||||||
 | 
					    const totalPersonal = Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0);
 | 
				
			||||||
 | 
					    const totalAmount = parseFloat(formData.amount);
 | 
				
			||||||
 | 
					    personalTotalError = totalPersonal > totalAmount;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if (!personalTotalError) {
 | 
				
			||||||
 | 
					      calculatePersonalEqualSplit();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<svelte:head>
 | 
					<svelte:head>
 | 
				
			||||||
@@ -323,28 +400,46 @@
 | 
				
			|||||||
    <div class="form-section">
 | 
					    <div class="form-section">
 | 
				
			||||||
      <h2>Split Between Users</h2>
 | 
					      <h2>Split Between Users</h2>
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      <div class="users-list">
 | 
					      {#if predefinedMode}
 | 
				
			||||||
        {#each users as user}
 | 
					        <div class="predefined-users">
 | 
				
			||||||
          <div class="user-item">
 | 
					          <p class="predefined-note">Splitting between predefined users:</p>
 | 
				
			||||||
            <span>{user}</span>
 | 
					          <div class="users-list">
 | 
				
			||||||
            {#if user !== data.session.user.nickname}
 | 
					            {#each users as user}
 | 
				
			||||||
              <button type="button" class="remove-user" on:click={() => removeUser(user)}>
 | 
					              <div class="user-item with-profile">
 | 
				
			||||||
                Remove
 | 
					                <ProfilePicture username={user} size={32} />
 | 
				
			||||||
              </button>
 | 
					                <span class="username">{user}</span>
 | 
				
			||||||
            {/if}
 | 
					                {#if user === data.session?.user?.nickname}
 | 
				
			||||||
 | 
					                  <span class="you-badge">You</span>
 | 
				
			||||||
 | 
					                {/if}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            {/each}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        {/each}
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      {:else}
 | 
				
			||||||
 | 
					        <div class="users-list">
 | 
				
			||||||
 | 
					          {#each users as user}
 | 
				
			||||||
 | 
					            <div class="user-item with-profile">
 | 
				
			||||||
 | 
					              <ProfilePicture username={user} size={32} />
 | 
				
			||||||
 | 
					              <span class="username">{user}</span>
 | 
				
			||||||
 | 
					              {#if user !== data.session.user.nickname}
 | 
				
			||||||
 | 
					                <button type="button" class="remove-user" on:click={() => removeUser(user)}>
 | 
				
			||||||
 | 
					                  Remove
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					              {/if}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {/each}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="add-user">
 | 
					        <div class="add-user">
 | 
				
			||||||
        <input 
 | 
					          <input 
 | 
				
			||||||
          type="text" 
 | 
					            type="text" 
 | 
				
			||||||
          bind:value={newUser} 
 | 
					            bind:value={newUser} 
 | 
				
			||||||
          placeholder="Add user..."
 | 
					            placeholder="Add user..."
 | 
				
			||||||
          on:keydown={(e) => e.key === 'Enter' && (e.preventDefault(), addUser())}
 | 
					            on:keydown={(e) => e.key === 'Enter' && (e.preventDefault(), addUser())}
 | 
				
			||||||
        />
 | 
					          />
 | 
				
			||||||
        <button type="button" on:click={addUser}>Add User</button>
 | 
					          <button type="button" on:click={addUser}>Add User</button>
 | 
				
			||||||
      </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="form-section">
 | 
					    <div class="form-section">
 | 
				
			||||||
@@ -353,7 +448,11 @@
 | 
				
			|||||||
      <div class="split-method">
 | 
					      <div class="split-method">
 | 
				
			||||||
        <label>
 | 
					        <label>
 | 
				
			||||||
          <input type="radio" bind:group={formData.splitMethod} value="equal" />
 | 
					          <input type="radio" bind:group={formData.splitMethod} value="equal" />
 | 
				
			||||||
          Equal Split
 | 
					          {predefinedMode && users.length === 2 ? 'Split 50/50' : 'Equal Split'}
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					        <label>
 | 
				
			||||||
 | 
					          <input type="radio" bind:group={formData.splitMethod} value="personal_equal" />
 | 
				
			||||||
 | 
					          Personal + Equal Split
 | 
				
			||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
        <label>
 | 
					        <label>
 | 
				
			||||||
          <input type="radio" bind:group={formData.splitMethod} value="full" />
 | 
					          <input type="radio" bind:group={formData.splitMethod} value="full" />
 | 
				
			||||||
@@ -382,12 +481,43 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      {/if}
 | 
					      {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {#if formData.splitMethod === 'personal_equal'}
 | 
				
			||||||
 | 
					        <div class="personal-splits">
 | 
				
			||||||
 | 
					          <h3>Personal Amounts</h3>
 | 
				
			||||||
 | 
					          <p class="description">Enter personal amounts for each user. The remainder will be split equally.</p>
 | 
				
			||||||
 | 
					          {#each users as user}
 | 
				
			||||||
 | 
					            <div class="split-input">
 | 
				
			||||||
 | 
					              <label>{user}</label>
 | 
				
			||||||
 | 
					              <input 
 | 
				
			||||||
 | 
					                type="number" 
 | 
				
			||||||
 | 
					                step="0.01" 
 | 
				
			||||||
 | 
					                min="0"
 | 
				
			||||||
 | 
					                bind:value={personalAmounts[user]}
 | 
				
			||||||
 | 
					                placeholder="0.00"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {/each}
 | 
				
			||||||
 | 
					          {#if formData.amount}
 | 
				
			||||||
 | 
					            <div class="remainder-info" class:error={personalTotalError}>
 | 
				
			||||||
 | 
					              <span>Total Personal: CHF {Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0).toFixed(2)}</span>
 | 
				
			||||||
 | 
					              <span>Remainder to Split: CHF {Math.max(0, parseFloat(formData.amount) - Object.values(personalAmounts).reduce((sum, val) => sum + (parseFloat(val) || 0), 0)).toFixed(2)}</span>
 | 
				
			||||||
 | 
					              {#if personalTotalError}
 | 
				
			||||||
 | 
					                <div class="error-message">⚠️ Personal amounts exceed total payment amount!</div>
 | 
				
			||||||
 | 
					              {/if}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {/if}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {#if Object.keys(splitAmounts).length > 0}
 | 
					      {#if Object.keys(splitAmounts).length > 0}
 | 
				
			||||||
        <div class="split-preview">
 | 
					        <div class="split-preview">
 | 
				
			||||||
          <h3>Split Preview</h3>
 | 
					          <h3>Split Preview</h3>
 | 
				
			||||||
          {#each users as user}
 | 
					          {#each users as user}
 | 
				
			||||||
            <div class="split-item">
 | 
					            <div class="split-item">
 | 
				
			||||||
              <span>{user}</span>
 | 
					              <div class="split-user">
 | 
				
			||||||
 | 
					                <ProfilePicture username={user} size={24} />
 | 
				
			||||||
 | 
					                <span class="username">{user}</span>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
              <span class="amount" class:positive={splitAmounts[user] < 0} class:negative={splitAmounts[user] > 0}>
 | 
					              <span class="amount" class:positive={splitAmounts[user] < 0} class:negative={splitAmounts[user] > 0}>
 | 
				
			||||||
                {#if splitAmounts[user] > 0}
 | 
					                {#if splitAmounts[user] > 0}
 | 
				
			||||||
                  owes CHF {splitAmounts[user].toFixed(2)}
 | 
					                  owes CHF {splitAmounts[user].toFixed(2)}
 | 
				
			||||||
@@ -564,6 +694,37 @@
 | 
				
			|||||||
    border-radius: 1rem;
 | 
					    border-radius: 1rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .user-item.with-profile {
 | 
				
			||||||
 | 
					    gap: 0.75rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .user-item .username {
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .you-badge {
 | 
				
			||||||
 | 
					    background-color: #1976d2;
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					    padding: 0.125rem 0.5rem;
 | 
				
			||||||
 | 
					    border-radius: 1rem;
 | 
				
			||||||
 | 
					    font-size: 0.75rem;
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .predefined-users {
 | 
				
			||||||
 | 
					    background-color: #f8f9fa;
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    border: 1px solid #e9ecef;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .predefined-note {
 | 
				
			||||||
 | 
					    margin: 0 0 1rem 0;
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .remove-user {
 | 
					  .remove-user {
 | 
				
			||||||
    background-color: #d32f2f;
 | 
					    background-color: #d32f2f;
 | 
				
			||||||
    color: white;
 | 
					    color: white;
 | 
				
			||||||
@@ -652,6 +813,12 @@
 | 
				
			|||||||
    margin-bottom: 0.5rem;
 | 
					    margin-bottom: 0.5rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .split-user {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    gap: 0.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .amount.positive {
 | 
					  .amount.positive {
 | 
				
			||||||
    color: #2e7d32;
 | 
					    color: #2e7d32;
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
@@ -709,6 +876,43 @@
 | 
				
			|||||||
    background-color: #e8e8e8;
 | 
					    background-color: #e8e8e8;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .personal-splits {
 | 
				
			||||||
 | 
					    margin-top: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .personal-splits .description {
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    margin-bottom: 1rem;
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .remainder-info {
 | 
				
			||||||
 | 
					    margin-top: 1rem;
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					    background-color: #f8f9fa;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    border: 1px solid #e9ecef;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .remainder-info.error {
 | 
				
			||||||
 | 
					    background-color: #fff5f5;
 | 
				
			||||||
 | 
					    border-color: #fed7d7;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .remainder-info span {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-message {
 | 
				
			||||||
 | 
					    color: #d32f2f;
 | 
				
			||||||
 | 
					    font-weight: 600;
 | 
				
			||||||
 | 
					    margin-top: 0.5rem;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media (max-width: 600px) {
 | 
					  @media (max-width: 600px) {
 | 
				
			||||||
    .add-payment {
 | 
					    .add-payment {
 | 
				
			||||||
      padding: 1rem;
 | 
					      padding: 1rem;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,6 +47,8 @@
 | 
				
			|||||||
      return `Split equally among ${payment.splits.length} people`;
 | 
					      return `Split equally among ${payment.splits.length} people`;
 | 
				
			||||||
    } else if (payment.splitMethod === 'full') {
 | 
					    } else if (payment.splitMethod === 'full') {
 | 
				
			||||||
      return `Paid in full by ${payment.paidBy}`;
 | 
					      return `Paid in full by ${payment.paidBy}`;
 | 
				
			||||||
 | 
					    } else if (payment.splitMethod === 'personal_equal') {
 | 
				
			||||||
 | 
					      return `Personal amounts + equal split among ${payment.splits.length} people`;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return `Custom split among ${payment.splits.length} people`;
 | 
					      return `Custom split among ${payment.splits.length} people`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user