Files
group-expenser/index.html
2025-03-28 16:41:23 +01:00

1620 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Group Payment Tracker</title>
<style>
:root {
/* Light theme variables */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #edf2f7;
--color-primary: #4a6da7;
--color-primary-dark: #345384;
--color-secondary: #57ba98;
--color-text: #2d3748;
--color-text-light: #4a5568;
--color-border: #e2e8f0;
--color-danger: #e53e3e;
--color-success: #38a169;
--color-warning: #ecc94b;
--color-info: #3182ce;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
[data-theme="dark"] {
/* Dark theme variables */
--bg-primary: #1a202c;
--bg-secondary: #2d3748;
--bg-tertiary: #283141;
--color-primary: #5a88c7;
--color-primary-dark: #4a6da7;
--color-secondary: #4fb08b;
--color-text: #e2e8f0;
--color-text-light: #a0aec0;
--color-border: #4a5568;
--color-danger: #f56565;
--color-success: #48bb78;
--color-warning: #f6ad55;
--color-info: #63b3ed;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.16);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
body {
background-color: var(--bg-secondary);
color: var(--color-text);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: var(--color-primary);
color: white;
padding: 20px 0;
margin-bottom: 30px;
box-shadow: var(--shadow-md);
position: relative;
}
header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
margin: 0;
font-weight: 600;
font-size: 1.75rem;
}
.theme-toggle {
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 1.2rem;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.theme-toggle:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.theme-toggle svg {
width: 24px;
height: 24px;
fill: currentColor;
}
.auth-section {
background-color: var(--bg-primary);
padding: 24px;
border-radius: 12px;
box-shadow: var(--shadow-md);
margin-bottom: 30px;
}
.card {
background-color: var(--bg-primary);
padding: 24px;
border-radius: 12px;
box-shadow: var(--shadow-md);
margin-bottom: 24px;
}
h2 {
color: var(--color-primary);
margin-bottom: 20px;
border-bottom: 2px solid var(--color-secondary);
padding-bottom: 12px;
font-weight: 600;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--color-text);
}
input, select, textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--color-border);
border-radius: 8px;
font-size: 16px;
background-color: var(--bg-primary);
color: var(--color-text);
transition: border-color 0.2s ease;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(74, 109, 167, 0.1);
}
.btn {
display: inline-block;
padding: 12px 24px;
border: none;
border-radius: 8px;
background-color: var(--color-primary);
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.2s, transform 0.1s;
}
.btn:hover {
background-color: var(--color-primary-dark);
}
.btn:active {
transform: translateY(1px);
}
.btn-success {
background-color: var(--color-success);
}
.btn-success:hover {
background-color: #2f9e5f;
}
.btn-danger {
background-color: var(--color-danger);
}
.btn-danger:hover {
background-color: #c53030;
}
.tabs {
display: flex;
margin-bottom: 24px;
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow-sm);
background-color: var(--bg-tertiary);
}
.tab {
padding: 14px 20px;
cursor: pointer;
background-color: var(--bg-tertiary);
color: var(--color-text-light);
flex-grow: 1;
text-align: center;
transition: all 0.2s ease;
}
.tab.active {
background-color: var(--bg-primary);
color: var(--color-primary);
font-weight: 600;
box-shadow: var(--shadow-sm);
}
.tab:hover:not(.active) {
background-color: var(--bg-secondary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow-sm);
}
table th, table td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
table th {
background-color: var(--color-primary);
color: white;
font-weight: 500;
}
table th:first-child {
border-top-left-radius: 8px;
}
table th:last-child {
border-top-right-radius: 8px;
}
table tbody tr {
background-color: var(--bg-primary);
}
table tbody tr:last-child td {
border-bottom: none;
}
table tbody tr:nth-child(even) {
background-color: var(--bg-tertiary);
}
table tbody tr:hover {
background-color: var(--bg-secondary);
}
.actions {
display: flex;
gap: 10px;
}
.btn-sm {
padding: 8px 12px;
font-size: 14px;
border-radius: 6px;
}
.summary-box {
background-color: var(--bg-tertiary);
border-left: 5px solid var(--color-info);
padding: 20px;
margin-bottom: 24px;
border-radius: 8px;
}
.summary-total {
font-size: 28px;
font-weight: 600;
color: var(--color-primary);
margin-top: 10px;
}
.alert {
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 500;
}
.alert-success {
background-color: rgba(56, 161, 105, 0.1);
color: var(--color-success);
border: 1px solid rgba(56, 161, 105, 0.2);
}
.alert-danger {
background-color: rgba(229, 62, 62, 0.1);
color: var(--color-danger);
border: 1px solid rgba(229, 62, 62, 0.2);
}
.hidden {
display: none;
}
.loading {
display: flex;
justify-content: center;
margin: 20px 0;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 36px;
height: 36px;
border-radius: 50%;
border-left: 4px solid var(--color-primary);
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Dark mode spinner adjustment */
[data-theme="dark"] .spinner {
border: 4px solid rgba(255, 255, 255, 0.1);
border-left: 4px solid var(--color-primary);
}
/* User list styles */
.user-list {
margin-top: 20px;
}
.user-item {
display: flex;
align-items: center;
margin-bottom: 12px;
background-color: var(--bg-tertiary);
padding: 12px 16px;
border-radius: 8px;
}
.user-item span {
flex-grow: 1;
}
.user-item .btn-remove {
background-color: var(--color-danger);
color: white;
border: none;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
}
.user-add {
display: flex;
margin-bottom: 20px;
gap: 12px;
}
.user-add input {
flex-grow: 1;
}
.user-add button {
background-color: var(--color-secondary);
color: white;
border: none;
border-radius: 8px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.user-add button:hover {
background-color: #3da682;
}
.price-per-person {
background-color: rgba(56, 161, 105, 0.1);
border-left: 4px solid var(--color-success);
padding: 16px;
margin-top: 20px;
border-radius: 8px;
}
.price-per-person p {
margin: 5px 0;
}
.price-calculation {
font-size: 18px;
font-weight: 600;
margin-top: 12px;
color: var(--color-text);
}
.user-toggle {
display: flex;
align-items: center;
margin-left: 15px;
}
.checkbox-container {
display: flex;
align-items: center;
margin-right: 15px;
}
.checkbox-container input[type="checkbox"] {
width: auto;
margin-right: 8px;
}
/* Payment mode toggle style */
.payment-mode {
margin-bottom: 20px;
background-color: var(--bg-tertiary);
padding: 16px;
border-radius: 8px;
}
.payment-mode label {
display: inline-block;
margin-right: 15px;
margin-bottom: 0;
}
.payment-mode input[type="radio"] {
width: auto;
margin-right: 8px;
}
/* Per-person input style */
.price-input-container {
display: flex;
align-items: center;
margin-top: 12px;
background-color: rgba(49, 130, 206, 0.1);
padding: 16px;
border-radius: 8px;
}
.price-input-container label {
margin-bottom: 0;
margin-right: 15px;
font-weight: bold;
}
.price-input-container input {
max-width: 150px;
}
.auto-calculate {
margin-left: 15px;
font-style: italic;
color: var(--color-info);
}
/* Responsive styles */
@media (max-width: 768px) {
.tabs {
flex-direction: column;
}
.tab {
border-radius: 0;
}
.tab:first-child {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.tab:last-child {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.actions {
flex-direction: column;
}
header .container {
flex-direction: column;
gap: 12px;
}
.theme-toggle {
position: absolute;
top: 20px;
right: 20px;
}
.price-input-container {
flex-direction: column;
align-items: flex-start;
}
.price-input-container label {
margin-bottom: 8px;
}
.auto-calculate {
margin-left: 0;
margin-top: 8px;
}
}
.price-calculation ul {
padding-left: 20px;
margin-top: 8px;
}
.price-calculation p {
margin-bottom: 4px;
}
</style>
</head>
<body>
<header>
<div class="container">
<h1>Group Payment Tracker</h1>
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark mode">
<svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-1.5v-17a8.5 8.5 0 0 0 0 17z"/>
</svg>
<svg id="sun-icon" class="hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/>
</svg>
</button>
</div>
</header>
<div class="container">
<div class="auth-section" id="auth-section">
<h2>Connect to Your Database</h2>
<div class="form-group">
<label for="api-url">API URL</label>
<input type="text" id="api-url" placeholder="e.g. https://baserow.io" value="https://baserow.io">
</div>
<div class="form-group">
<label for="api-token">API Token</label>
<input type="password" id="api-token" placeholder="Enter your database token">
</div>
<div class="form-group">
<input type="checkbox" id="remember-me" checked>
<label for="remember-me" style="display: inline-block;">Remember credentials</label>
</div>
<button id="connect-btn" class="btn">Connect</button>
</div>
<div id="app-content" class="hidden">
<div class="tabs">
<div class="tab active" data-tab="group-payments">Group Payments</div>
<div class="tab" data-tab="individual-payments">Individual Payments</div>
<div class="tab" data-tab="summary">Summary</div>
</div>
<div id="alerts"></div>
<div class="tab-content active" id="group-payments">
<div class="card">
<h2>Add New Group Payment</h2>
<form id="add-group-payment-form">
<div class="form-group">
<label for="group-name">Payment Name</label>
<input type="text" id="group-name" required>
</div>
<!-- Payment mode selection -->
<div class="form-group payment-mode">
<label>Payment Mode:</label>
<div>
<input type="radio" id="split-equal" name="payment-mode" value="split-equal" checked>
<label for="split-equal">Equal Split (Auto-calculate)</label>
<input type="radio" id="per-person" name="payment-mode" value="per-person">
<label for="per-person">Custom Per-Person</label>
</div>
<!-- Per-person price input for equal split -->
<div id="per-person-container" class="price-input-container">
<label for="per-person-price">Price per person (€):</label>
<input type="number" id="per-person-price" step="0.01" min="0">
<span class="auto-calculate" id="total-auto-calculate"></span>
</div>
</div>
<div class="form-group">
<label for="group-amount">Total Amount</label>
<input type="number" id="group-amount" step="0.01" min="0" required>
</div>
<div class="form-group">
<label for="group-notes">Notes</label>
<textarea id="group-notes" rows="3"></textarea>
</div>
<div class="form-group">
<label for="group-paid">Paid Overall</label>
<select id="group-paid">
<option value="true">Yes</option>
<option value="false" selected>No</option>
</select>
</div>
<!-- New user management section -->
<div class="form-group">
<label>Split Between</label>
<div class="user-add">
<input type="text" id="new-user" placeholder="Add person...">
<button type="button" id="add-user-btn">Add</button>
</div>
<div class="user-list" id="user-list">
<!-- User items will be added here -->
<div class="user-item">
<span>You (already paid)</span>
<div class="user-toggle">
<div class="checkbox-container">
<input type="checkbox" id="is-me-paid" checked disabled>
<label for="is-me-paid">Paid</label>
</div>
</div>
<button type="button" class="btn-remove" disabled></button>
</div>
</div>
<div class="price-per-person">
<p>Total users: <span id="total-users">1</span></p>
<p>Each person pays: <span id="price-per-person">€0.00</span></p>
<div class="price-calculation">
<p>Total cost breakdown:</p>
<ul id="cost-breakdown">
<li>You: €0.00</li>
</ul>
</div>
</div>
</div>
<button type="submit" class="btn btn-success">Add Payment & Create Individual Expenses</button>
</form>
</div>
<div class="card">
<h2>Group Payments List</h2>
<div class="loading" id="group-loading">
<div class="spinner"></div>
</div>
<table id="group-payments-table">
<thead>
<tr>
<th>Name</th>
<th>Amount</th>
<th>Paid</th>
<th>Notes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="group-payments-list">
<!-- Payments will be listed here -->
</tbody>
</table>
</div>
</div>
<div class="tab-content" id="individual-payments">
<div class="card">
<h2>Add Individual Payment</h2>
<form id="add-individual-payment-form">
<div class="form-group">
<label for="individual-for">For</label>
<input type="text" id="individual-for" required>
</div>
<div class="form-group">
<label for="individual-amount">Amount</label>
<input type="number" id="individual-amount" step="0.01" min="0" required>
</div>
<div class="form-group">
<label for="individual-notes">Notes</label>
<textarea id="individual-notes" rows="3"></textarea>
</div>
<div class="form-group">
<label for="individual-paid">Paid</label>
<select id="individual-paid">
<option value="true">Yes</option>
<option value="false" selected>No</option>
</select>
</div>
<div class="form-group">
<label for="individual-group">Related Group Payment</label>
<select id="individual-group">
<!-- Group payment options will be loaded here -->
</select>
</div>
<button type="submit" class="btn btn-success">Add Individual Payment</button>
</form>
</div>
<div class="card">
<h2>Individual Payments List</h2>
<div class="loading" id="individual-loading">
<div class="spinner"></div>
</div>
<table id="individual-payments-table">
<thead>
<tr>
<th>For</th>
<th>Amount</th>
<th>Paid</th>
<th>Notes</th>
<th>Group Payment</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="individual-payments-list">
<!-- Individual payments will be listed here -->
</tbody>
</table>
</div>
</div>
<div class="tab-content" id="summary">
<div class="card">
<h2>Payment Summary</h2>
<div class="summary-box">
<p>Total Group Payments</p>
<div class="summary-total" id="total-group">€0.00</div>
</div>
<div class="summary-box">
<p>Total Individual Payments</p>
<div class="summary-total" id="total-individual">€0.00</div>
</div>
<div class="summary-box">
<p>Remaining to Pay</p>
<div class="summary-total" id="remaining">€0.00</div>
</div>
</div>
<div class="card">
<h2>Payment Status Per Person</h2>
<table id="summary-table">
<thead>
<tr>
<th>Person</th>
<th>Total Amount</th>
<th>Paid</th>
<th>Remaining</th>
</tr>
</thead>
<tbody id="summary-list">
<!-- Summary data will be listed here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// Global variables
let apiUrl = '';
let apiToken = '';
let groupPayments = [];
let individualPayments = [];
// DOM elements
const authSection = document.getElementById('auth-section');
const appContent = document.getElementById('app-content');
const connectBtn = document.getElementById('connect-btn');
const apiUrlInput = document.getElementById('api-url');
const apiTokenInput = document.getElementById('api-token');
const rememberMeCheckbox = document.getElementById('remember-me');
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
const alertsContainer = document.getElementById('alerts');
const themeToggle = document.getElementById('theme-toggle');
const moonIcon = document.getElementById('moon-icon');
const sunIcon = document.getElementById('sun-icon');
// Forms
const addGroupPaymentForm = document.getElementById('add-group-payment-form');
const addIndividualPaymentForm = document.getElementById('add-individual-payment-form');
// Tables and loading indicators
const groupPaymentsList = document.getElementById('group-payments-list');
const individualPaymentsList = document.getElementById('individual-payments-list');
const groupLoading = document.getElementById('group-loading');
const individualLoading = document.getElementById('individual-loading');
const individualGroupSelect = document.getElementById('individual-group');
// Summary elements
const totalGroupEl = document.getElementById('total-group');
const totalIndividualEl = document.getElementById('total-individual');
const remainingEl = document.getElementById('remaining');
const summaryList = document.getElementById('summary-list');
// User list elements
const newUserInput = document.getElementById('new-user');
const addUserBtn = document.getElementById('add-user-btn');
const userList = document.getElementById('user-list');
const totalUsersEl = document.getElementById('total-users');
const pricePerPersonEl = document.getElementById('price-per-person');
const costBreakdownEl = document.getElementById('cost-breakdown');
const groupAmountInput = document.getElementById('group-amount');
// Payment mode selection
const splitEqualRadio = document.getElementById('split-equal');
const perPersonRadio = document.getElementById('per-person');
const perPersonPriceInput = document.getElementById('per-person-price');
const perPersonContainer = document.getElementById('per-person-container');
const totalAutoCalculate = document.getElementById('total-auto-calculate');
// List of users (You are always included by default)
const users = [{ name: 'You', isPaid: true, isYou: true, amount: 0 }];
// Theme handling
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Update theme toggle icons
if (newTheme === 'dark') {
moonIcon.classList.add('hidden');
sunIcon.classList.remove('hidden');
} else {
moonIcon.classList.remove('hidden');
sunIcon.classList.add('hidden');
}
}
// Set initial theme based on preference
function setInitialTheme() {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = savedTheme || (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
// Set correct icon
if (theme === 'dark') {
moonIcon.classList.add('hidden');
sunIcon.classList.remove('hidden');
} else {
moonIcon.classList.remove('hidden');
sunIcon.classList.add('hidden');
}
}
// Add theme toggle event listener
themeToggle.addEventListener('click', toggleTheme);
// Event Listeners
connectBtn.addEventListener('click', connectToAPI);
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.getAttribute('data-tab');
activateTab(tabId);
});
});
addGroupPaymentForm.addEventListener('submit', addGroupPayment);
addIndividualPaymentForm.addEventListener('submit', addIndividualPayment);
addUserBtn.addEventListener('click', addUser);
newUserInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addUser();
}
});
groupAmountInput.addEventListener('input', calculatePricePerPerson);
perPersonPriceInput.addEventListener('input', updateTotalFromPerPersonPrice);
splitEqualRadio.addEventListener('change', onPaymentModeChange);
perPersonRadio.addEventListener('change', onPaymentModeChange);
function onPaymentModeChange() {
const paymentMode = document.querySelector('input[name="payment-mode"]:checked').value;
if (paymentMode === 'split-equal') {
perPersonContainer.style.display = 'flex';
if (!perPersonPriceInput.value && groupAmountInput.value) {
const totalAmount = parseFloat(groupAmountInput.value) || 0;
const pricePerPerson = totalAmount / users.length;
perPersonPriceInput.value = pricePerPerson.toFixed(2);
totalAutoCalculate.textContent = `Total: €${totalAmount.toFixed(2)} (${pricePerPerson.toFixed(2)} × ${users.length})`;
} else {
updateTotalFromPerPersonPrice();
}
} else {
perPersonContainer.style.display = 'none';
calculatePricePerPerson();
}
renderUsers();
}
function updateTotalFromPerPersonPrice() {
const pricePerPerson = parseFloat(perPersonPriceInput.value) || 0;
const totalUsers = users.length;
const totalAmount = pricePerPerson * totalUsers;
groupAmountInput.value = totalAmount.toFixed(2);
totalAutoCalculate.textContent = `Total: €${totalAmount.toFixed(2)} (${pricePerPerson.toFixed(2)} × ${totalUsers})`;
calculatePricePerPerson();
}
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}
function getCookie(name) {
const cookieName = name + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const cookieArray = decodedCookie.split(';');
for (let i = 0; i < cookieArray.length; i++) {
let cookie = cookieArray[i];
while (cookie.charAt(0) === ' ') {
cookie = cookie.substring(1);
}
if (cookie.indexOf(cookieName) === 0) {
return cookie.substring(cookieName.length, cookie.length);
}
}
return "";
}
function checkSavedCredentials() {
const savedUrl = getCookie("apiUrl");
const savedToken = getCookie("apiToken");
if (savedUrl && savedToken) {
apiUrlInput.value = savedUrl;
apiTokenInput.value = savedToken;
connectToAPI();
}
}
function connectToAPI() {
apiUrl = apiUrlInput.value.trim();
apiToken = apiTokenInput.value.trim();
if (!apiUrl || !apiToken) {
showAlert('Please enter both API URL and token.', 'danger');
return;
}
fetchData(`${apiUrl}/api/database/fields/table/682/`, apiToken)
.then(data => {
if (data && Array.isArray(data)) {
if (rememberMeCheckbox.checked) {
setCookie("apiUrl", apiUrl, 30);
setCookie("apiToken", apiToken, 30);
}
authSection.classList.add('hidden');
appContent.classList.remove('hidden');
loadGroupPayments();
loadIndividualPayments();
} else {
showAlert('Failed to connect. Please check your API URL and token.', 'danger');
}
})
.catch(error => {
showAlert(`Connection error: ${error.message}`, 'danger');
});
}
async function fetchData(url, token, options = {}) {
try {
const defaultOptions = {
headers: {
'Authorization': `Token ${token}`,
'Content-Type': 'application/json'
}
};
const fetchOptions = {...defaultOptions, ...options};
const response = await fetch(url, fetchOptions);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.description || response.statusText);
}
return await response.json();
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
function loadGroupPayments() {
groupLoading.style.display = 'flex';
fetchData(`${apiUrl}/api/database/rows/table/682/?user_field_names=true`, apiToken)
.then(data => {
groupPayments = data.results;
renderGroupPayments();
populateGroupPaymentsDropdown();
updateSummary();
})
.catch(error => {
showAlert(`Failed to load group payments: ${error.message}`, 'danger');
})
.finally(() => {
groupLoading.style.display = 'none';
});
}
function loadIndividualPayments() {
individualLoading.style.display = 'flex';
fetchData(`${apiUrl}/api/database/rows/table/684/?user_field_names=true`, apiToken)
.then(data => {
individualPayments = data.results;
renderIndividualPayments();
updateSummary();
})
.catch(error => {
showAlert(`Failed to load individual payments: ${error.message}`, 'danger');
})
.finally(() => {
individualLoading.style.display = 'none';
});
}
function renderGroupPayments() {
groupPaymentsList.innerHTML = '';
if (groupPayments.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="5" style="text-align: center;">No group payments found</td>';
groupPaymentsList.appendChild(row);
return;
}
groupPayments.forEach(payment => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${payment.Name || '-'}</td>
<td>€${parseFloat(payment.Money).toFixed(2)}</td>
<td>${payment['Paid overall'] ? '✅' : '❌'}</td>
<td>${payment.Notes || '-'}</td>
<td class="actions">
<button class="btn btn-sm btn-danger" onclick="deleteGroupPayment(${payment.id})">Delete</button>
<button class="btn btn-sm" onclick="toggleGroupPaid(${payment.id}, ${!payment['Paid overall']})">
${payment['Paid overall'] ? 'Mark Unpaid' : 'Mark Paid'}
</button>
</td>
`;
groupPaymentsList.appendChild(row);
});
}
function renderIndividualPayments() {
individualPaymentsList.innerHTML = '';
if (individualPayments.length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="6" style="text-align: center;">No individual payments found</td>';
individualPaymentsList.appendChild(row);
return;
}
individualPayments.forEach(payment => {
const groupPayment = payment['Group payments'] && payment['Group payments'].length > 0
? payment['Group payments'][0].value : '-';
const row = document.createElement('tr');
row.innerHTML = `
<td>${payment.For || '-'}</td>
<td>€${parseFloat(payment.Money).toFixed(2)}</td>
<td>${payment.Paid ? '✅' : '❌'}</td>
<td>${payment.Notes || '-'}</td>
<td>${groupPayment}</td>
<td class="actions">
<button class="btn btn-sm btn-danger" onclick="deleteIndividualPayment(${payment.id})">Delete</button>
<button class="btn btn-sm" onclick="toggleIndividualPaid(${payment.id}, ${!payment.Paid})">
${payment.Paid ? 'Mark Unpaid' : 'Mark Paid'}
</button>
</td>
`;
individualPaymentsList.appendChild(row);
});
}
function populateGroupPaymentsDropdown() {
individualGroupSelect.innerHTML = '<option value="">None</option>';
groupPayments.forEach(payment => {
const option = document.createElement('option');
option.value = payment.id;
option.textContent = payment.Name;
individualGroupSelect.appendChild(option);
});
}
function addUser() {
const userName = newUserInput.value.trim();
if (userName && !users.some(user => user.name.toLowerCase() === userName.toLowerCase())) {
users.push({ name: userName, isPaid: false, isYou: false, amount: 0 });
newUserInput.value = '';
renderUsers();
calculatePricePerPerson();
} else if (!userName) {
showAlert('Please enter a name', 'danger');
} else {
showAlert('This person is already in the list', 'danger');
}
}
function removeUser(index) {
if (!users[index].isYou) {
users.splice(index, 1);
renderUsers();
calculatePricePerPerson();
}
}
function togglePaid(index) {
if (!users[index].isYou) {
users[index].isPaid = !users[index].isPaid;
renderUsers();
calculatePricePerPerson();
}
}
function updateUserAmount(index, amount) {
if (!users[index].isYou) {
users[index].amount = parseFloat(amount) || 0;
calculatePricePerPerson();
}
}
function renderUsers() {
userList.innerHTML = '';
const paymentMode = document.querySelector('input[name="payment-mode"]:checked').value;
users.forEach((user, index) => {
const userItem = document.createElement('div');
userItem.className = 'user-item';
let userHtml = `
<span>${user.name}</span>
<div class="user-toggle">
<div class="checkbox-container">
<input type="checkbox" id="is-paid-${index}"
${user.isPaid ? 'checked' : ''}
${user.isYou ? 'disabled' : ''}>
<label for="is-paid-${index}">Paid</label>
</div>`;
if (paymentMode === 'per-person' && !user.isYou) {
userHtml += `
<div class="amount-container" style="margin-left: 10px;">
<input type="number" id="amount-${index}" placeholder="Amount"
min="0" step="0.01" value="${user.amount}"
style="width: 100px; padding: 5px;">
</div>`;
}
userHtml += `
</div>
<button type="button" class="btn-remove" ${user.isYou ? 'disabled' : ''}>✕</button>
`;
userItem.innerHTML = userHtml;
userList.appendChild(userItem);
if (!user.isYou) {
const checkbox = userItem.querySelector(`#is-paid-${index}`);
const removeBtn = userItem.querySelector('.btn-remove');
checkbox.addEventListener('change', () => togglePaid(index));
removeBtn.addEventListener('click', () => removeUser(index));
if (paymentMode === 'per-person') {
const amountInput = userItem.querySelector(`#amount-${index}`);
amountInput.addEventListener('input', (e) => updateUserAmount(index, e.target.value));
}
}
});
totalUsersEl.textContent = users.length;
}
function calculatePricePerPerson() {
const totalAmount = parseFloat(groupAmountInput.value) || 0;
const paymentMode = document.querySelector('input[name="payment-mode"]:checked').value;
renderUsers();
if (totalAmount > 0) {
if (paymentMode === 'split-equal') {
const totalUsers = users.length;
const pricePerPerson = parseFloat(perPersonPriceInput.value) || (totalAmount / totalUsers);
if (perPersonPriceInput.value === '' || Math.abs(pricePerPerson * totalUsers - totalAmount) > 0.01) {
perPersonPriceInput.value = (totalAmount / totalUsers).toFixed(2);
totalAutoCalculate.textContent = `Total: €${totalAmount.toFixed(2)} (${perPersonPriceInput.value} × ${totalUsers})`;
}
pricePerPersonEl.textContent = `${pricePerPerson.toFixed(2)}`;
costBreakdownEl.innerHTML = '';
users.forEach(user => {
const li = document.createElement('li');
if (user.isYou) {
li.textContent = `${user.name}: €${pricePerPerson.toFixed(2)} (already paid)`;
} else if (user.isPaid) {
li.textContent = `${user.name}: €${pricePerPerson.toFixed(2)} (paid)`;
} else {
li.textContent = `${user.name}: €${pricePerPerson.toFixed(2)} (unpaid)`;
}
costBreakdownEl.appendChild(li);
});
} else {
pricePerPersonEl.textContent = `Varies (set individually)`;
let assignedTotal = 0;
users.forEach(user => {
if (!user.isYou) {
assignedTotal += user.amount;
}
});
const yourAmount = Math.max(0, totalAmount - assignedTotal);
users[0].amount = yourAmount;
costBreakdownEl.innerHTML = '';
users.forEach(user => {
const li = document.createElement('li');
if (user.isYou) {
li.textContent = `${user.name}: €${yourAmount.toFixed(2)} (already paid)`;
} else if (user.isPaid) {
li.textContent = `${user.name}: €${user.amount.toFixed(2)} (paid)`;
} else {
li.textContent = `${user.name}: €${user.amount.toFixed(2)} (unpaid)`;
}
costBreakdownEl.appendChild(li);
});
}
} else {
pricePerPersonEl.textContent = '€0.00';
costBreakdownEl.innerHTML = '<li>You: €0.00</li>';
}
}
function addGroupPayment(e) {
e.preventDefault();
const name = document.getElementById('group-name').value;
const amount = document.getElementById('group-amount').value;
const notes = document.getElementById('group-notes').value;
const paid = document.getElementById('group-paid').value === 'true';
const paymentMode = document.querySelector('input[name="payment-mode"]:checked').value;
if (users.length <= 1) {
showAlert('Please add at least one more person to split with', 'warning');
return;
}
const paymentData = {
"Name": name,
"Money": amount,
"Notes": notes,
"Paid overall": paid
};
fetchData(
`${apiUrl}/api/database/rows/table/682/?user_field_names=true`,
apiToken,
{
method: 'POST',
body: JSON.stringify(paymentData)
}
)
.then((response) => {
showAlert('Group payment added successfully!', 'success');
const newGroupPaymentId = response.id;
let individualPromises = [];
if (paymentMode === 'split-equal') {
const pricePerPerson = parseFloat(perPersonPriceInput.value);
individualPromises = users
.filter(user => !user.isYou)
.map(user => {
const individualData = {
"For": user.name,
"Money": pricePerPerson.toFixed(2),
"Notes": `Share of ${name}`,
"Paid": user.isPaid,
"Group payments": [newGroupPaymentId]
};
return fetchData(
`${apiUrl}/api/database/rows/table/684/?user_field_names=true`,
apiToken,
{
method: 'POST',
body: JSON.stringify(individualData)
}
);
});
} else {
individualPromises = users
.filter(user => !user.isYou && user.amount > 0)
.map(user => {
const individualData = {
"For": user.name,
"Money": user.amount.toFixed(2),
"Notes": `Share of ${name}`,
"Paid": user.isPaid,
"Group payments": [newGroupPaymentId]
};
return fetchData(
`${apiUrl}/api/database/rows/table/684/?user_field_names=true`,
apiToken,
{
method: 'POST',
body: JSON.stringify(individualData)
}
);
});
}
return Promise.all(individualPromises);
})
.then(() => {
showAlert('All individual payments created!', 'success');
addGroupPaymentForm.reset();
users.length = 1;
users[0].amount = 0;
renderUsers();
calculatePricePerPerson();
splitEqualRadio.checked = true;
perPersonContainer.style.display = 'flex';
perPersonPriceInput.value = '';
totalAutoCalculate.textContent = '';
loadGroupPayments();
loadIndividualPayments();
})
.catch(error => {
showAlert(`Failed to add payments: ${error.message}`, 'danger');
});
}
function addIndividualPayment(e) {
e.preventDefault();
const forPerson = document.getElementById('individual-for').value;
const amount = document.getElementById('individual-amount').value;
const notes = document.getElementById('individual-notes').value;
const paid = document.getElementById('individual-paid').value === 'true';
const groupId = document.getElementById('individual-group').value;
const paymentData = {
"For": forPerson,
"Money": amount,
"Notes": notes,
"Paid": paid,
"Group payments": groupId ? [parseInt(groupId)] : []
};
fetchData(
`${apiUrl}/api/database/rows/table/684/?user_field_names=true`,
apiToken,
{
method: 'POST',
body: JSON.stringify(paymentData)
}
)
.then(() => {
showAlert('Individual payment added successfully!', 'success');
addIndividualPaymentForm.reset();
loadIndividualPayments();
})
.catch(error => {
showAlert(`Failed to add individual payment: ${error.message}`, 'danger');
});
}
function deleteGroupPayment(id) {
if (confirm('Are you sure you want to delete this group payment?')) {
fetchData(
`${apiUrl}/api/database/rows/table/682/${id}/`,
apiToken,
{
method: 'DELETE'
}
)
.then(() => {
showAlert('Group payment deleted successfully!', 'success');
loadGroupPayments();
loadIndividualPayments();
})
.catch(error => {
showAlert(`Failed to delete group payment: ${error.message}`, 'danger');
});
}
}
function deleteIndividualPayment(id) {
if (confirm('Are you sure you want to delete this individual payment?')) {
fetchData(
`${apiUrl}/api/database/rows/table/684/${id}/`,
apiToken,
{
method: 'DELETE'
}
)
.then(() => {
showAlert('Individual payment deleted successfully!', 'success');
loadIndividualPayments();
})
.catch(error => {
showAlert(`Failed to delete individual payment: ${error.message}`, 'danger');
});
}
}
function toggleGroupPaid(id, status) {
fetchData(
`${apiUrl}/api/database/rows/table/682/${id}/?user_field_names=true`,
apiToken,
{
method: 'PATCH',
body: JSON.stringify({"Paid overall": status})
}
)
.then(() => {
showAlert(`Group payment marked as ${status ? 'paid' : 'unpaid'}!`, 'success');
loadGroupPayments();
})
.catch(error => {
showAlert(`Failed to update payment status: ${error.message}`, 'danger');
});
}
function toggleIndividualPaid(id, status) {
fetchData(
`${apiUrl}/api/database/rows/table/684/${id}/?user_field_names=true`,
apiToken,
{
method: 'PATCH',
body: JSON.stringify({"Paid": status})
}
)
.then(() => {
showAlert(`Individual payment marked as ${status ? 'paid' : 'unpaid'}!`, 'success');
loadIndividualPayments();
})
.catch(error => {
showAlert(`Failed to update payment status: ${error.message}`, 'danger');
});
}
function updateSummary() {
let totalGroup = 0;
let totalIndividual = 0;
let totalUnpaid = 0;
groupPayments.forEach(payment => {
totalGroup += parseFloat(payment.Money);
});
individualPayments.forEach(payment => {
const amount = parseFloat(payment.Money);
totalIndividual += amount;
if (!payment.Paid) {
totalUnpaid += amount;
}
});
totalGroupEl.textContent = `${totalGroup.toFixed(2)}`;
totalIndividualEl.textContent = `${totalIndividual.toFixed(2)}`;
remainingEl.textContent = `${totalUnpaid.toFixed(2)}`;
const personSummary = {};
individualPayments.forEach(payment => {
const person = payment.For;
if (!personSummary[person]) {
personSummary[person] = {
total: 0,
paid: 0
};
}
const amount = parseFloat(payment.Money);
personSummary[person].total += amount;
if (payment.Paid) {
personSummary[person].paid += amount;
}
});
summaryList.innerHTML = '';
if (Object.keys(personSummary).length === 0) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="4" style="text-align: center;">No data available</td>';
summaryList.appendChild(row);
return;
}
Object.keys(personSummary).forEach(person => {
const summary = personSummary[person];
const row = document.createElement('tr');
const remaining = summary.total - summary.paid;
row.innerHTML = `
<td>${person}</td>
<td>€${summary.total.toFixed(2)}</td>
<td>€${summary.paid.toFixed(2)}</td>
<td>€${remaining.toFixed(2)}</td>
`;
summaryList.appendChild(row);
});
}
function showAlert(message, type) {
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
alertsContainer.appendChild(alert);
setTimeout(() => {
alert.remove();
}, 5000);
}
function activateTab(tabId) {
tabs.forEach(tab => {
if (tab.getAttribute('data-tab') === tabId) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
tabContents.forEach(content => {
if (content.id === tabId) {
content.classList.add('active');
} else {
content.classList.remove('active');
}
});
if (tabId === 'summary') {
updateSummary();
}
}
function init() {
setInitialTheme();
renderUsers();
onPaymentModeChange();
checkSavedCredentials();
}
init();
</script>
</body>
</html>