mirror of
https://github.com/SrIzan10/group-expenser.git
synced 2026-06-06 00:56:51 +00:00
1611 lines
60 KiB
HTML
1611 lines
60 KiB
HTML
<!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;
|
||
}
|
||
}
|
||
</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> |