Fix payment display and dashboard refresh functionality
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				CI / update (push) Failing after 4s
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	CI / update (push) Failing after 4s
				
			- Fix 'paid in full for others' payments showing CHF 0.00 instead of actual amount - Add time-based sorting to payments (date + createdAt) for proper chronological order - Redirect to dashboard after adding payment instead of payments list - Implement complete dashboard refresh after payment deletion via modal - Fix dashboard component reactivity for single debtor view updates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		@@ -43,6 +43,11 @@
 | 
				
			|||||||
      currency: 'CHF'
 | 
					      currency: 'CHF'
 | 
				
			||||||
    }).format(amount);
 | 
					    }).format(amount);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Export refresh method for parent components to call
 | 
				
			||||||
 | 
					  export async function refresh() {
 | 
				
			||||||
 | 
					    await fetchDebtBreakdown();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if !shouldHide}
 | 
					{#if !shouldHide}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,8 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: {
 | 
					  $: {
 | 
				
			||||||
    // Recalculate when debtData changes
 | 
					    // Recalculate when debtData changes - trigger on the arrays specifically
 | 
				
			||||||
 | 
					    const totalUsers = debtData.whoOwesMe.length + debtData.whoIOwe.length;
 | 
				
			||||||
    singleDebtUser = getSingleDebtUser();
 | 
					    singleDebtUser = getSingleDebtUser();
 | 
				
			||||||
    shouldShowIntegratedView = singleDebtUser !== null;
 | 
					    shouldShowIntegratedView = singleDebtUser !== null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -67,7 +68,12 @@
 | 
				
			|||||||
      if (!response.ok) {
 | 
					      if (!response.ok) {
 | 
				
			||||||
        throw new Error('Failed to fetch balance');
 | 
					        throw new Error('Failed to fetch balance');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      balance = await response.json();
 | 
					      const newBalance = await response.json();
 | 
				
			||||||
 | 
					      // Force reactivity by creating new object with spread arrays
 | 
				
			||||||
 | 
					      balance = {
 | 
				
			||||||
 | 
					        netBalance: newBalance.netBalance || 0,
 | 
				
			||||||
 | 
					        recentSplits: [...(newBalance.recentSplits || [])]
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      error = err.message;
 | 
					      error = err.message;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -79,7 +85,14 @@
 | 
				
			|||||||
      if (!response.ok) {
 | 
					      if (!response.ok) {
 | 
				
			||||||
        throw new Error('Failed to fetch debt breakdown');
 | 
					        throw new Error('Failed to fetch debt breakdown');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      debtData = await response.json();
 | 
					      const newDebtData = await response.json();
 | 
				
			||||||
 | 
					      // Force reactivity by creating new object with spread arrays
 | 
				
			||||||
 | 
					      debtData = {
 | 
				
			||||||
 | 
					        whoOwesMe: [...(newDebtData.whoOwesMe || [])],
 | 
				
			||||||
 | 
					        whoIOwe: [...(newDebtData.whoIOwe || [])],
 | 
				
			||||||
 | 
					        totalOwedToMe: newDebtData.totalOwedToMe || 0,
 | 
				
			||||||
 | 
					        totalIOwe: newDebtData.totalIOwe || 0
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      error = err.message;
 | 
					      error = err.message;
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
@@ -94,6 +107,12 @@
 | 
				
			|||||||
    }).format(Math.abs(amount));
 | 
					    }).format(Math.abs(amount));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Export refresh method for parent components to call
 | 
				
			||||||
 | 
					  export async function refresh() {
 | 
				
			||||||
 | 
					    loading = true;
 | 
				
			||||||
 | 
					    await Promise.all([fetchBalance(), fetchDebtBreakdown()]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="balance-cards">
 | 
					<div class="balance-cards">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -66,7 +66,7 @@ export const GET: RequestHandler = async ({ locals, url }) => {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        { $unwind: '$paymentId' },
 | 
					        { $unwind: '$paymentId' },
 | 
				
			||||||
        { $sort: { 'paymentId.date': -1 } },
 | 
					        { $sort: { 'paymentId.date': -1, 'paymentId.createdAt': -1 } },
 | 
				
			||||||
        { $limit: 10 }
 | 
					        { $limit: 10 }
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@ export const GET: RequestHandler = async ({ locals, url }) => {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const payments = await Payment.find()
 | 
					    const payments = await Payment.find()
 | 
				
			||||||
      .populate('splits')
 | 
					      .populate('splits')
 | 
				
			||||||
      .sort({ date: -1 })
 | 
					      .sort({ date: -1, createdAt: -1 })
 | 
				
			||||||
      .limit(limit)
 | 
					      .limit(limit)
 | 
				
			||||||
      .skip(offset)
 | 
					      .skip(offset)
 | 
				
			||||||
      .lean();
 | 
					      .lean();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,17 @@
 | 
				
			|||||||
      paymentId = null;
 | 
					      paymentId = null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async function handlePaymentDeleted() {
 | 
				
			||||||
 | 
					    // Close the modal
 | 
				
			||||||
 | 
					    showModal = false;
 | 
				
			||||||
 | 
					    paymentId = null;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Dispatch a custom event to trigger dashboard refresh
 | 
				
			||||||
 | 
					    if ($page.route.id === '/cospend') {
 | 
				
			||||||
 | 
					      window.dispatchEvent(new CustomEvent('dashboardRefresh'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="layout-container" class:has-modal={showModal}>
 | 
					<div class="layout-container" class:has-modal={showModal}>
 | 
				
			||||||
@@ -36,7 +47,7 @@
 | 
				
			|||||||
      <div class="modal-content">
 | 
					      <div class="modal-content">
 | 
				
			||||||
        {#key paymentId}
 | 
					        {#key paymentId}
 | 
				
			||||||
          <div in:fly={{x: 50, duration: 300, easing: quintOut}} out:fly={{x: -50, duration: 300, easing: quintOut}}>
 | 
					          <div in:fly={{x: 50, duration: 300, easing: quintOut}} out:fly={{x: -50, duration: 300, easing: quintOut}}>
 | 
				
			||||||
            <PaymentModal {paymentId} on:close={() => showModal = false} />
 | 
					            <PaymentModal {paymentId} on:close={() => showModal = false} on:paymentDeleted={handlePaymentDeleted} />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        {/key}
 | 
					        {/key}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,11 +18,27 @@
 | 
				
			|||||||
  let loading = false; // Start as false since we have server data
 | 
					  let loading = false; // Start as false since we have server data
 | 
				
			||||||
  let error = null;
 | 
					  let error = null;
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					  // Component references for refreshing
 | 
				
			||||||
 | 
					  let enhancedBalanceComponent;
 | 
				
			||||||
 | 
					  let debtBreakdownComponent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Progressive enhancement: refresh data if JavaScript is available
 | 
					  // Progressive enhancement: refresh data if JavaScript is available
 | 
				
			||||||
  onMount(async () => {
 | 
					  onMount(async () => {
 | 
				
			||||||
    // Mark that JavaScript is loaded for progressive enhancement
 | 
					    // Mark that JavaScript is loaded for progressive enhancement
 | 
				
			||||||
    document.body.classList.add('js-loaded');
 | 
					    document.body.classList.add('js-loaded');
 | 
				
			||||||
    await fetchBalance();
 | 
					    await fetchBalance();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Listen for dashboard refresh events from the layout
 | 
				
			||||||
 | 
					    const handleDashboardRefresh = () => {
 | 
				
			||||||
 | 
					      refreshAllComponents();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    window.addEventListener('dashboardRefresh', handleDashboardRefresh);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Cleanup
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      window.removeEventListener('dashboardRefresh', handleDashboardRefresh);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function fetchBalance() {
 | 
					  async function fetchBalance() {
 | 
				
			||||||
@@ -40,6 +56,22 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Function to refresh all dashboard components after payment deletion
 | 
				
			||||||
 | 
					  async function refreshAllComponents() {
 | 
				
			||||||
 | 
					    // Refresh the main balance and recent activity
 | 
				
			||||||
 | 
					    await fetchBalance();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Refresh the enhanced balance component if it exists and has a refresh method
 | 
				
			||||||
 | 
					    if (enhancedBalanceComponent && enhancedBalanceComponent.refresh) {
 | 
				
			||||||
 | 
					      await enhancedBalanceComponent.refresh();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Refresh the debt breakdown component if it exists and has a refresh method  
 | 
				
			||||||
 | 
					    if (debtBreakdownComponent && debtBreakdownComponent.refresh) {
 | 
				
			||||||
 | 
					      await debtBreakdownComponent.refresh();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function formatCurrency(amount) {
 | 
					  function formatCurrency(amount) {
 | 
				
			||||||
    return new Intl.NumberFormat('de-CH', {
 | 
					    return new Intl.NumberFormat('de-CH', {
 | 
				
			||||||
      style: 'currency',
 | 
					      style: 'currency',
 | 
				
			||||||
@@ -98,18 +130,17 @@
 | 
				
			|||||||
    <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 initialBalance={data.balance} initialDebtData={data.debtData} />
 | 
					  <EnhancedBalance bind:this={enhancedBalanceComponent} initialBalance={data.balance} initialDebtData={data.debtData} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="actions">
 | 
					  <div class="actions">
 | 
				
			||||||
    <a href="/cospend/payments/add" class="btn btn-primary">Add Payment</a>
 | 
					    <a href="/cospend/payments/add" class="btn btn-primary">Add Payment</a>
 | 
				
			||||||
    <a href="/cospend/payments" class="btn btn-secondary">View All Payments</a>
 | 
					    <a href="/cospend/payments" class="btn btn-secondary">View All Payments</a>
 | 
				
			||||||
    <a href="/cospend/recurring" class="btn btn-recurring">Recurring Payments</a>
 | 
					 | 
				
			||||||
    {#if balance.netBalance !== 0}
 | 
					    {#if balance.netBalance !== 0}
 | 
				
			||||||
      <a href="/cospend/settle" class="btn btn-settlement">Settle Debts</a>
 | 
					      <a href="/cospend/settle" class="btn btn-settlement">Settle Debts</a>
 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <DebtBreakdown />
 | 
					  <DebtBreakdown bind:this={debtBreakdownComponent} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {#if loading}
 | 
					  {#if loading}
 | 
				
			||||||
    <div class="loading">Loading recent activity...</div>
 | 
					    <div class="loading">Loading recent activity...</div>
 | 
				
			||||||
@@ -175,7 +206,7 @@
 | 
				
			|||||||
                      {:else if split.amount < 0}
 | 
					                      {:else if split.amount < 0}
 | 
				
			||||||
                        +{formatCurrency(split.amount)}
 | 
					                        +{formatCurrency(split.amount)}
 | 
				
			||||||
                      {:else}
 | 
					                      {:else}
 | 
				
			||||||
                        even
 | 
					                        {formatCurrency(split.amount)}
 | 
				
			||||||
                      {/if}
 | 
					                      {/if}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
@@ -282,16 +313,6 @@
 | 
				
			|||||||
    background-color: #e8e8e8;
 | 
					    background-color: #e8e8e8;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .btn-recurring {
 | 
					 | 
				
			||||||
    background: linear-gradient(135deg, #9c27b0, #673ab7);
 | 
					 | 
				
			||||||
    color: white;
 | 
					 | 
				
			||||||
    border: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .btn-recurring:hover {
 | 
					 | 
				
			||||||
    background: linear-gradient(135deg, #8e24aa, #5e35b1);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .btn-settlement {
 | 
					  .btn-settlement {
 | 
				
			||||||
    background: linear-gradient(135deg, #28a745, #20c997);
 | 
					    background: linear-gradient(135deg, #28a745, #20c997);
 | 
				
			||||||
    color: white;
 | 
					    color: white;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -122,6 +122,7 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="header-actions">
 | 
					    <div class="header-actions">
 | 
				
			||||||
      <a href="/cospend/payments/add" class="btn btn-primary">Add Payment</a>
 | 
					      <a href="/cospend/payments/add" class="btn btn-primary">Add Payment</a>
 | 
				
			||||||
 | 
					      <a href="/cospend/recurring" class="btn btn-recurring">Recurring Payments</a>
 | 
				
			||||||
      <a href="/cospend" class="btn btn-secondary">Back to Dashboard</a>
 | 
					      <a href="/cospend" class="btn btn-secondary">Back to Dashboard</a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -214,7 +215,7 @@
 | 
				
			|||||||
                      {:else if split.amount < 0}
 | 
					                      {:else if split.amount < 0}
 | 
				
			||||||
                        owed {formatCurrency(Math.abs(split.amount))}
 | 
					                        owed {formatCurrency(Math.abs(split.amount))}
 | 
				
			||||||
                      {:else}
 | 
					                      {:else}
 | 
				
			||||||
                        even
 | 
					                        owes {formatCurrency(split.amount)}
 | 
				
			||||||
                      {/if}
 | 
					                      {/if}
 | 
				
			||||||
                    </span>
 | 
					                    </span>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
@@ -335,6 +336,16 @@
 | 
				
			|||||||
    background-color: #e8e8e8;
 | 
					    background-color: #e8e8e8;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-recurring {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #9c27b0, #673ab7);
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-recurring:hover {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, #8e24aa, #5e35b1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .loading, .error {
 | 
					  .loading, .error {
 | 
				
			||||||
    text-align: center;
 | 
					    text-align: center;
 | 
				
			||||||
    padding: 2rem;
 | 
					    padding: 2rem;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,13 @@ export const actions: Actions = {
 | 
				
			|||||||
    const category = formData.get('category')?.toString() || 'groceries';
 | 
					    const category = formData.get('category')?.toString() || 'groceries';
 | 
				
			||||||
    const splitMethod = formData.get('splitMethod')?.toString() || 'equal';
 | 
					    const splitMethod = formData.get('splitMethod')?.toString() || 'equal';
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Recurring payment data
 | 
				
			||||||
 | 
					    const isRecurring = formData.get('isRecurring') === 'true';
 | 
				
			||||||
 | 
					    const recurringFrequency = formData.get('recurringFrequency')?.toString() || 'monthly';
 | 
				
			||||||
 | 
					    const recurringCronExpression = formData.get('recurringCronExpression')?.toString() || '';
 | 
				
			||||||
 | 
					    const recurringStartDate = formData.get('recurringStartDate')?.toString() || '';
 | 
				
			||||||
 | 
					    const recurringEndDate = formData.get('recurringEndDate')?.toString() || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Basic validation
 | 
					    // Basic validation
 | 
				
			||||||
    if (!title || amount <= 0 || !paidBy) {
 | 
					    if (!title || amount <= 0 || !paidBy) {
 | 
				
			||||||
      return fail(400, {
 | 
					      return fail(400, {
 | 
				
			||||||
@@ -41,6 +48,16 @@ export const actions: Actions = {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Recurring payment validation
 | 
				
			||||||
 | 
					    if (isRecurring) {
 | 
				
			||||||
 | 
					      if (recurringFrequency === 'custom' && !recurringCronExpression) {
 | 
				
			||||||
 | 
					        return fail(400, {
 | 
				
			||||||
 | 
					          error: 'Please provide a cron expression for custom recurring payments',
 | 
				
			||||||
 | 
					          values: Object.fromEntries(formData)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // Get users from form - either predefined or manual
 | 
					      // Get users from form - either predefined or manual
 | 
				
			||||||
      const users = [];
 | 
					      const users = [];
 | 
				
			||||||
@@ -83,10 +100,13 @@ export const actions: Actions = {
 | 
				
			|||||||
          amount: user === paidBy ? paidByAmount : splitAmount
 | 
					          amount: user === paidBy ? paidByAmount : splitAmount
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
      } else if (splitMethod === 'full') {
 | 
					      } else if (splitMethod === 'full') {
 | 
				
			||||||
        // Payer pays everything, others owe nothing
 | 
					        // Payer pays everything, others owe their share of the full amount
 | 
				
			||||||
 | 
					        const otherUsers = users.filter(user => user !== paidBy);
 | 
				
			||||||
 | 
					        const amountPerOtherUser = otherUsers.length > 0 ? amount / otherUsers.length : 0;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        splits = users.map(user => ({
 | 
					        splits = users.map(user => ({
 | 
				
			||||||
          username: user,
 | 
					          username: user,
 | 
				
			||||||
          amount: user === paidBy ? -amount : 0
 | 
					          amount: user === paidBy ? -amount : amountPerOtherUser
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
      } else if (splitMethod === 'personal_equal') {
 | 
					      } else if (splitMethod === 'personal_equal') {
 | 
				
			||||||
        // Get personal amounts from form
 | 
					        // Get personal amounts from form
 | 
				
			||||||
@@ -158,8 +178,43 @@ export const actions: Actions = {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Success - redirect to payments list
 | 
					      const paymentResult = await response.json();
 | 
				
			||||||
      throw redirect(303, '/cospend/payments');
 | 
					
 | 
				
			||||||
 | 
					      // If this is a recurring payment, create the recurring payment record
 | 
				
			||||||
 | 
					      if (isRecurring) {
 | 
				
			||||||
 | 
					        const recurringPayload = {
 | 
				
			||||||
 | 
					          title,
 | 
				
			||||||
 | 
					          description,
 | 
				
			||||||
 | 
					          amount,
 | 
				
			||||||
 | 
					          paidBy,
 | 
				
			||||||
 | 
					          category,
 | 
				
			||||||
 | 
					          splitMethod,
 | 
				
			||||||
 | 
					          splits,
 | 
				
			||||||
 | 
					          frequency: recurringFrequency,
 | 
				
			||||||
 | 
					          cronExpression: recurringFrequency === 'custom' ? recurringCronExpression : undefined,
 | 
				
			||||||
 | 
					          startDate: recurringStartDate ? new Date(recurringStartDate).toISOString() : new Date().toISOString(),
 | 
				
			||||||
 | 
					          endDate: recurringEndDate ? new Date(recurringEndDate).toISOString() : null,
 | 
				
			||||||
 | 
					          isActive: true,
 | 
				
			||||||
 | 
					          nextExecutionDate: recurringStartDate ? new Date(recurringStartDate).toISOString() : new Date().toISOString()
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const recurringResponse = await fetch('/api/cospend/recurring-payments', {
 | 
				
			||||||
 | 
					          method: 'POST',
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json'
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          body: JSON.stringify(recurringPayload)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!recurringResponse.ok) {
 | 
				
			||||||
 | 
					          // Log the error but don't fail the entire operation since the payment was created
 | 
				
			||||||
 | 
					          console.error('Failed to create recurring payment:', await recurringResponse.text());
 | 
				
			||||||
 | 
					          // Could optionally return a warning to the user
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Success - redirect to dashboard
 | 
				
			||||||
 | 
					      throw redirect(303, '/cospend');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      if (error.status === 303) throw error; // Re-throw redirect
 | 
					      if (error.status === 303) throw error; // Re-throw redirect
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
  import { enhance } from '$app/forms';
 | 
					  import { enhance } from '$app/forms';
 | 
				
			||||||
  import { getCategoryOptions } from '$lib/utils/categories';
 | 
					  import { getCategoryOptions } from '$lib/utils/categories';
 | 
				
			||||||
  import { PREDEFINED_USERS, isPredefinedUsersMode } from '$lib/config/users';
 | 
					  import { PREDEFINED_USERS, isPredefinedUsersMode } from '$lib/config/users';
 | 
				
			||||||
 | 
					  import { validateCronExpression, getFrequencyDescription, calculateNextExecutionDate } from '$lib/utils/recurring';
 | 
				
			||||||
  import ProfilePicture from '$lib/components/ProfilePicture.svelte';
 | 
					  import ProfilePicture from '$lib/components/ProfilePicture.svelte';
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  export let data;
 | 
					  export let data;
 | 
				
			||||||
@@ -18,7 +19,16 @@
 | 
				
			|||||||
    date: form?.values?.date || new Date().toISOString().split('T')[0],
 | 
					    date: form?.values?.date || new Date().toISOString().split('T')[0],
 | 
				
			||||||
    category: form?.values?.category || 'groceries',
 | 
					    category: form?.values?.category || 'groceries',
 | 
				
			||||||
    splitMethod: form?.values?.splitMethod || 'equal',
 | 
					    splitMethod: form?.values?.splitMethod || 'equal',
 | 
				
			||||||
    splits: []
 | 
					    splits: [],
 | 
				
			||||||
 | 
					    isRecurring: form?.values?.isRecurring === 'true' || false
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Recurring payment settings
 | 
				
			||||||
 | 
					  let recurringData = {
 | 
				
			||||||
 | 
					    frequency: form?.values?.recurringFrequency || 'monthly',
 | 
				
			||||||
 | 
					    cronExpression: form?.values?.recurringCronExpression || '',
 | 
				
			||||||
 | 
					    startDate: form?.values?.recurringStartDate || new Date().toISOString().split('T')[0],
 | 
				
			||||||
 | 
					    endDate: form?.values?.recurringEndDate || ''
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let imageFile = null;
 | 
					  let imageFile = null;
 | 
				
			||||||
@@ -31,6 +41,8 @@
 | 
				
			|||||||
  let personalTotalError = false;
 | 
					  let personalTotalError = false;
 | 
				
			||||||
  let predefinedMode = data.predefinedUsers.length > 0;
 | 
					  let predefinedMode = data.predefinedUsers.length > 0;
 | 
				
			||||||
  let jsEnhanced = false;
 | 
					  let jsEnhanced = false;
 | 
				
			||||||
 | 
					  let cronError = false;
 | 
				
			||||||
 | 
					  let nextExecutionPreview = '';
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  // Initialize users from server data for no-JS support
 | 
					  // Initialize users from server data for no-JS support
 | 
				
			||||||
  let users = predefinedMode ? [...data.predefinedUsers] : (data.currentUser ? [data.currentUser] : []);
 | 
					  let users = predefinedMode ? [...data.predefinedUsers] : (data.currentUser ? [data.currentUser] : []);
 | 
				
			||||||
@@ -172,12 +184,14 @@
 | 
				
			|||||||
    if (!formData.amount) return;
 | 
					    if (!formData.amount) return;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const amountNum = parseFloat(formData.amount);
 | 
					    const amountNum = parseFloat(formData.amount);
 | 
				
			||||||
 | 
					    const otherUsers = users.filter(user => user !== formData.paidBy);
 | 
				
			||||||
 | 
					    const amountPerOtherUser = otherUsers.length > 0 ? amountNum / otherUsers.length : 0;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    users.forEach(user => {
 | 
					    users.forEach(user => {
 | 
				
			||||||
      if (user === formData.paidBy) {
 | 
					      if (user === formData.paidBy) {
 | 
				
			||||||
        splitAmounts[user] = -amountNum; // They paid it all, so they're owed the full amount
 | 
					        splitAmounts[user] = -amountNum; // They paid it all, so they're owed the full amount
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        splitAmounts[user] = 0; // Others don't owe anything
 | 
					        splitAmounts[user] = amountPerOtherUser; // Others owe their share of the full amount
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    splitAmounts = { ...splitAmounts };
 | 
					    splitAmounts = { ...splitAmounts };
 | 
				
			||||||
@@ -336,6 +350,46 @@
 | 
				
			|||||||
      calculatePersonalEqualSplit();
 | 
					      calculatePersonalEqualSplit();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function validateCron() {
 | 
				
			||||||
 | 
					    if (recurringData.frequency !== 'custom') {
 | 
				
			||||||
 | 
					      cronError = false;
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    cronError = !validateCronExpression(recurringData.cronExpression);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function updateNextExecutionPreview() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (recurringData.frequency && recurringData.startDate && formData.isRecurring) {
 | 
				
			||||||
 | 
					        const recurringPayment = {
 | 
				
			||||||
 | 
					          ...recurringData,
 | 
				
			||||||
 | 
					          startDate: new Date(recurringData.startDate)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const nextDate = calculateNextExecutionDate(recurringPayment, new Date(recurringData.startDate));
 | 
				
			||||||
 | 
					        nextExecutionPreview = nextDate.toLocaleString('de-CH', {
 | 
				
			||||||
 | 
					          weekday: 'long',
 | 
				
			||||||
 | 
					          year: 'numeric',
 | 
				
			||||||
 | 
					          month: 'long',
 | 
				
			||||||
 | 
					          day: 'numeric',
 | 
				
			||||||
 | 
					          hour: '2-digit',
 | 
				
			||||||
 | 
					          minute: '2-digit'
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        nextExecutionPreview = '';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      nextExecutionPreview = 'Invalid configuration';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: if (recurringData.cronExpression) {
 | 
				
			||||||
 | 
					    validateCron();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: if (recurringData.frequency || recurringData.cronExpression || recurringData.startDate || formData.isRecurring) {
 | 
				
			||||||
 | 
					    updateNextExecutionPreview();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<svelte:head>
 | 
					<svelte:head>
 | 
				
			||||||
@@ -419,8 +473,102 @@
 | 
				
			|||||||
          {/each}
 | 
					          {/each}
 | 
				
			||||||
        </select>
 | 
					        </select>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="form-group">
 | 
				
			||||||
 | 
					        <label class="checkbox-label">
 | 
				
			||||||
 | 
					          <input 
 | 
				
			||||||
 | 
					            type="checkbox" 
 | 
				
			||||||
 | 
					            name="isRecurring" 
 | 
				
			||||||
 | 
					            bind:checked={formData.isRecurring}
 | 
				
			||||||
 | 
					            value="true"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          Make this a recurring payment
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {#if formData.isRecurring}
 | 
				
			||||||
 | 
					      <div class="form-section">
 | 
				
			||||||
 | 
					        <h2>Recurring Payment</h2>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="recurring-options">
 | 
				
			||||||
 | 
					          <div class="form-row">
 | 
				
			||||||
 | 
					            <div class="form-group">
 | 
				
			||||||
 | 
					              <label for="frequency">Frequency *</label>
 | 
				
			||||||
 | 
					              <select id="frequency" name="recurringFrequency" bind:value={recurringData.frequency} required>
 | 
				
			||||||
 | 
					                <option value="daily">Daily</option>
 | 
				
			||||||
 | 
					                <option value="weekly">Weekly</option>
 | 
				
			||||||
 | 
					                <option value="monthly">Monthly</option>
 | 
				
			||||||
 | 
					                <option value="quarterly">Quarterly</option>
 | 
				
			||||||
 | 
					                <option value="yearly">Yearly</option>
 | 
				
			||||||
 | 
					                <option value="custom">Custom (Cron)</option>
 | 
				
			||||||
 | 
					              </select>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="form-group">
 | 
				
			||||||
 | 
					              <label for="recurringStartDate">Start Date *</label>
 | 
				
			||||||
 | 
					              <input 
 | 
				
			||||||
 | 
					                type="date" 
 | 
				
			||||||
 | 
					                id="recurringStartDate" 
 | 
				
			||||||
 | 
					                name="recurringStartDate"
 | 
				
			||||||
 | 
					                bind:value={recurringData.startDate}
 | 
				
			||||||
 | 
					                required
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {#if recurringData.frequency === 'custom'}
 | 
				
			||||||
 | 
					            <div class="form-group">
 | 
				
			||||||
 | 
					              <label for="recurringCronExpression">Cron Expression *</label>
 | 
				
			||||||
 | 
					              <input 
 | 
				
			||||||
 | 
					                type="text" 
 | 
				
			||||||
 | 
					                id="recurringCronExpression" 
 | 
				
			||||||
 | 
					                name="recurringCronExpression"
 | 
				
			||||||
 | 
					                bind:value={recurringData.cronExpression} 
 | 
				
			||||||
 | 
					                required 
 | 
				
			||||||
 | 
					                placeholder="0 9 * * 1  (Every Monday at 9:00 AM)"
 | 
				
			||||||
 | 
					                class:error={cronError}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					              <div class="help-text">
 | 
				
			||||||
 | 
					                <p>Cron format: minute hour day-of-month month day-of-week</p>
 | 
				
			||||||
 | 
					                <p>Examples:</p>
 | 
				
			||||||
 | 
					                <ul>
 | 
				
			||||||
 | 
					                  <li><code>0 9 * * *</code> - Every day at 9:00 AM</li>
 | 
				
			||||||
 | 
					                  <li><code>0 9 1 * *</code> - Every 1st of the month at 9:00 AM</li>
 | 
				
			||||||
 | 
					                  <li><code>0 9 * * 1</code> - Every Monday at 9:00 AM</li>
 | 
				
			||||||
 | 
					                  <li><code>0 9 1,15 * *</code> - 1st and 15th of every month at 9:00 AM</li>
 | 
				
			||||||
 | 
					                </ul>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              {#if cronError}
 | 
				
			||||||
 | 
					                <div class="field-error">Invalid cron expression</div>
 | 
				
			||||||
 | 
					              {/if}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div class="form-group">
 | 
				
			||||||
 | 
					            <label for="recurringEndDate">End Date (optional)</label>
 | 
				
			||||||
 | 
					            <input 
 | 
				
			||||||
 | 
					              type="date" 
 | 
				
			||||||
 | 
					              id="recurringEndDate" 
 | 
				
			||||||
 | 
					              name="recurringEndDate"
 | 
				
			||||||
 | 
					              bind:value={recurringData.endDate}
 | 
				
			||||||
 | 
					              min={recurringData.startDate}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <small class="help-text">Leave empty for indefinite recurring</small>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {#if nextExecutionPreview}
 | 
				
			||||||
 | 
					            <div class="execution-preview">
 | 
				
			||||||
 | 
					              <h3>Next Execution</h3>
 | 
				
			||||||
 | 
					              <p class="next-execution">{nextExecutionPreview}</p>
 | 
				
			||||||
 | 
					              <p class="frequency-description">{getFrequencyDescription(recurringData)}</p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {/if}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="form-section">
 | 
					    <div class="form-section">
 | 
				
			||||||
      <h2>Receipt Image</h2>
 | 
					      <h2>Receipt Image</h2>
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
@@ -524,23 +672,14 @@
 | 
				
			|||||||
    <div class="form-section">
 | 
					    <div class="form-section">
 | 
				
			||||||
      <h2>Split Method</h2>
 | 
					      <h2>Split Method</h2>
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      <div class="split-method">
 | 
					      <div class="form-group">
 | 
				
			||||||
        <label>
 | 
					        <label for="splitMethod">How should this payment be split?</label>
 | 
				
			||||||
          <input type="radio" name="splitMethod" value="equal" bind:group={formData.splitMethod} />
 | 
					        <select id="splitMethod" name="splitMethod" bind:value={formData.splitMethod} required>
 | 
				
			||||||
          {predefinedMode && users.length === 2 ? 'Split 50/50' : 'Equal Split'}
 | 
					          <option value="equal">{predefinedMode && users.length === 2 ? 'Split 50/50' : 'Equal Split'}</option>
 | 
				
			||||||
        </label>
 | 
					          <option value="personal_equal">Personal + Equal Split</option>
 | 
				
			||||||
        <label>
 | 
					          <option value="full">{paidInFullText}</option>
 | 
				
			||||||
          <input type="radio" name="splitMethod" value="personal_equal" bind:group={formData.splitMethod} />
 | 
					          <option value="proportional">Custom Proportions</option>
 | 
				
			||||||
          Personal + Equal Split
 | 
					        </select>
 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
        <label>
 | 
					 | 
				
			||||||
          <input type="radio" name="splitMethod" value="full" bind:group={formData.splitMethod} />
 | 
					 | 
				
			||||||
          {paidInFullText}
 | 
					 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
        <label>
 | 
					 | 
				
			||||||
          <input type="radio" name="splitMethod" value="proportional" bind:group={formData.splitMethod} />
 | 
					 | 
				
			||||||
          Custom Proportions
 | 
					 | 
				
			||||||
        </label>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {#if formData.splitMethod === 'proportional'}
 | 
					      {#if formData.splitMethod === 'proportional'}
 | 
				
			||||||
@@ -605,7 +744,7 @@
 | 
				
			|||||||
                {:else if splitAmounts[user] < 0}
 | 
					                {:else if splitAmounts[user] < 0}
 | 
				
			||||||
                  is owed CHF {Math.abs(splitAmounts[user]).toFixed(2)}
 | 
					                  is owed CHF {Math.abs(splitAmounts[user]).toFixed(2)}
 | 
				
			||||||
                {:else}
 | 
					                {:else}
 | 
				
			||||||
                  even
 | 
					                  owes CHF {splitAmounts[user].toFixed(2)}
 | 
				
			||||||
                {/if}
 | 
					                {/if}
 | 
				
			||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
@@ -623,7 +762,7 @@
 | 
				
			|||||||
        Cancel
 | 
					        Cancel
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
      <button type="submit" class="btn-primary" disabled={loading}>
 | 
					      <button type="submit" class="btn-primary" disabled={loading}>
 | 
				
			||||||
        {loading ? 'Creating...' : 'Create Payment'}
 | 
					        {loading ? 'Creating...' : (formData.isRecurring ? 'Create Recurring Payment' : 'Create Payment')}
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </form>
 | 
					  </form>
 | 
				
			||||||
@@ -834,19 +973,6 @@
 | 
				
			|||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .split-method {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
    gap: 0.75rem;
 | 
					 | 
				
			||||||
    margin-bottom: 1rem;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .split-method label {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    align-items: center;
 | 
					 | 
				
			||||||
    gap: 0.5rem;
 | 
					 | 
				
			||||||
    cursor: pointer;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .proportional-splits {
 | 
					  .proportional-splits {
 | 
				
			||||||
    border: 1px solid #ddd;
 | 
					    border: 1px solid #ddd;
 | 
				
			||||||
@@ -1027,6 +1153,96 @@
 | 
				
			|||||||
    color: #666;
 | 
					    color: #666;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* Recurring payment styles */
 | 
				
			||||||
 | 
					  .checkbox-label {
 | 
				
			||||||
 | 
					    display: flex !important;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    gap: 0.5rem;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .checkbox-label input[type="checkbox"] {
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .recurring-options {
 | 
				
			||||||
 | 
					    margin-top: 1rem;
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					    background-color: #f8f9fa;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    border: 1px solid #e9ecef;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .help-text {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    margin-top: 0.25rem;
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .help-text p {
 | 
				
			||||||
 | 
					    margin: 0.5rem 0 0.25rem 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .help-text code {
 | 
				
			||||||
 | 
					    background-color: #f5f5f5;
 | 
				
			||||||
 | 
					    padding: 0.125rem 0.25rem;
 | 
				
			||||||
 | 
					    border-radius: 0.25rem;
 | 
				
			||||||
 | 
					    font-family: monospace;
 | 
				
			||||||
 | 
					    font-size: 0.85em;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .help-text ul {
 | 
				
			||||||
 | 
					    margin: 0.5rem 0;
 | 
				
			||||||
 | 
					    padding-left: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .help-text li {
 | 
				
			||||||
 | 
					    margin-bottom: 0.25rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .field-error {
 | 
				
			||||||
 | 
					    color: #d32f2f;
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					    margin-top: 0.25rem;
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input.error {
 | 
				
			||||||
 | 
					    border-color: #d32f2f;
 | 
				
			||||||
 | 
					    box-shadow: 0 0 0 2px rgba(211, 47, 47, 0.2);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .execution-preview {
 | 
				
			||||||
 | 
					    background-color: #e3f2fd;
 | 
				
			||||||
 | 
					    border: 1px solid #2196f3;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					    margin-top: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .execution-preview h3 {
 | 
				
			||||||
 | 
					    margin: 0 0 0.5rem 0;
 | 
				
			||||||
 | 
					    color: #1976d2;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .next-execution {
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					    font-weight: 600;
 | 
				
			||||||
 | 
					    color: #1976d2;
 | 
				
			||||||
 | 
					    margin: 0.5rem 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .frequency-description {
 | 
				
			||||||
 | 
					    color: #666;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media (max-width: 600px) {
 | 
					  @media (max-width: 600px) {
 | 
				
			||||||
    .add-payment {
 | 
					    .add-payment {
 | 
				
			||||||
      padding: 1rem;
 | 
					      padding: 1rem;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -259,7 +259,7 @@
 | 
				
			|||||||
                  {:else if split.amount < 0}
 | 
					                  {:else if split.amount < 0}
 | 
				
			||||||
                    owed CHF {Math.abs(split.amount).toFixed(2)}
 | 
					                    owed CHF {Math.abs(split.amount).toFixed(2)}
 | 
				
			||||||
                  {:else}
 | 
					                  {:else}
 | 
				
			||||||
                    even
 | 
					                    owes CHF {split.amount.toFixed(2)}
 | 
				
			||||||
                  {/if}
 | 
					                  {/if}
 | 
				
			||||||
                </span>
 | 
					                </span>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -191,7 +191,7 @@
 | 
				
			|||||||
                  {:else if split.amount < 0}
 | 
					                  {:else if split.amount < 0}
 | 
				
			||||||
                    owed {formatCurrency(split.amount)}
 | 
					                    owed {formatCurrency(split.amount)}
 | 
				
			||||||
                  {:else}
 | 
					                  {:else}
 | 
				
			||||||
                    even
 | 
					                    owes {formatCurrency(split.amount)}
 | 
				
			||||||
                  {/if}
 | 
					                  {/if}
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -195,7 +195,7 @@
 | 
				
			|||||||
                    {:else if split.amount < 0}
 | 
					                    {:else if split.amount < 0}
 | 
				
			||||||
                      gets {formatCurrency(split.amount)}
 | 
					                      gets {formatCurrency(split.amount)}
 | 
				
			||||||
                    {:else}
 | 
					                    {:else}
 | 
				
			||||||
                      even
 | 
					                      owes {formatCurrency(split.amount)}
 | 
				
			||||||
                    {/if}
 | 
					                    {/if}
 | 
				
			||||||
                  </span>
 | 
					                  </span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -100,12 +100,14 @@
 | 
				
			|||||||
    if (!formData.amount) return;
 | 
					    if (!formData.amount) return;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const amountNum = parseFloat(formData.amount);
 | 
					    const amountNum = parseFloat(formData.amount);
 | 
				
			||||||
 | 
					    const otherUsers = users.filter(user => user !== formData.paidBy);
 | 
				
			||||||
 | 
					    const amountPerOtherUser = otherUsers.length > 0 ? amountNum / otherUsers.length : 0;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    users.forEach(user => {
 | 
					    users.forEach(user => {
 | 
				
			||||||
      if (user === formData.paidBy) {
 | 
					      if (user === formData.paidBy) {
 | 
				
			||||||
        splitAmounts[user] = -amountNum;
 | 
					        splitAmounts[user] = -amountNum; // They paid it all, so they're owed the full amount
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        splitAmounts[user] = 0;
 | 
					        splitAmounts[user] = amountPerOtherUser; // Others owe their share of the full amount
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    splitAmounts = { ...splitAmounts };
 | 
					    splitAmounts = { ...splitAmounts };
 | 
				
			||||||
@@ -535,7 +537,7 @@
 | 
				
			|||||||
                {:else if splitAmounts[user] < 0}
 | 
					                {:else if splitAmounts[user] < 0}
 | 
				
			||||||
                  is owed CHF {Math.abs(splitAmounts[user]).toFixed(2)}
 | 
					                  is owed CHF {Math.abs(splitAmounts[user]).toFixed(2)}
 | 
				
			||||||
                {:else}
 | 
					                {:else}
 | 
				
			||||||
                  even
 | 
					                  owes CHF {splitAmounts[user].toFixed(2)}
 | 
				
			||||||
                {/if}
 | 
					                {/if}
 | 
				
			||||||
              </span>
 | 
					              </span>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -135,12 +135,14 @@
 | 
				
			|||||||
    if (!formData.amount) return;
 | 
					    if (!formData.amount) return;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    const amountNum = parseFloat(formData.amount);
 | 
					    const amountNum = parseFloat(formData.amount);
 | 
				
			||||||
 | 
					    const otherUsers = users.filter(user => user !== formData.paidBy);
 | 
				
			||||||
 | 
					    const amountPerOtherUser = otherUsers.length > 0 ? amountNum / otherUsers.length : 0;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    users.forEach(user => {
 | 
					    users.forEach(user => {
 | 
				
			||||||
      if (user === formData.paidBy) {
 | 
					      if (user === formData.paidBy) {
 | 
				
			||||||
        splitAmounts[user] = -amountNum;
 | 
					        splitAmounts[user] = -amountNum; // They paid it all, so they're owed the full amount
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        splitAmounts[user] = 0;
 | 
					        splitAmounts[user] = amountPerOtherUser; // Others owe their share of the full amount
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    splitAmounts = { ...splitAmounts };
 | 
					    splitAmounts = { ...splitAmounts };
 | 
				
			||||||
@@ -573,7 +575,7 @@
 | 
				
			|||||||
                  {:else if splitAmounts[user] < 0}
 | 
					                  {:else if splitAmounts[user] < 0}
 | 
				
			||||||
                    is owed CHF {Math.abs(splitAmounts[user]).toFixed(2)}
 | 
					                    is owed CHF {Math.abs(splitAmounts[user]).toFixed(2)}
 | 
				
			||||||
                  {:else}
 | 
					                  {:else}
 | 
				
			||||||
                    even
 | 
					                    owes CHF {splitAmounts[user].toFixed(2)}
 | 
				
			||||||
                  {/if}
 | 
					                  {/if}
 | 
				
			||||||
                </span>
 | 
					                </span>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user