mirror of
https://github.com/SrIzan10/group-expenser.git
synced 2026-06-06 00:56:51 +00:00
2295 lines
84 KiB
HTML
2295 lines
84 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;
|
||
}
|
||
}
|
||
|
||
.price-calculation ul {
|
||
padding-left: 20px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.price-calculation p {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* File upload styles */
|
||
.file-upload {
|
||
border: 2px dashed var(--color-border);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
position: relative;
|
||
cursor: pointer;
|
||
background-color: var(--bg-tertiary);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.file-upload:hover {
|
||
border-color: var(--color-primary);
|
||
background-color: rgba(74, 109, 167, 0.05);
|
||
}
|
||
|
||
.file-upload input[type="file"] {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
top: 0;
|
||
left: 0;
|
||
opacity: 0;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.file-upload p {
|
||
margin: 0;
|
||
color: var(--color-text-light);
|
||
}
|
||
|
||
.file-upload-icon {
|
||
font-size: 2rem;
|
||
margin-bottom: 10px;
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
.file-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 15px 0 0 0;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
background-color: var(--bg-secondary);
|
||
border-radius: 6px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.file-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.file-name {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 250px;
|
||
}
|
||
|
||
.file-size {
|
||
color: var(--color-text-light);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.file-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-preview-btn, .file-remove-btn {
|
||
background: none;
|
||
border: none;
|
||
padding: 4px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.file-preview-btn {
|
||
color: var(--color-info);
|
||
}
|
||
|
||
.file-remove-btn {
|
||
color: var(--color-danger);
|
||
}
|
||
|
||
.file-preview-btn:hover, .file-remove-btn:hover {
|
||
background-color: var(--bg-tertiary);
|
||
}
|
||
|
||
.upload-progress {
|
||
width: 100%;
|
||
height: 6px;
|
||
margin-top: 10px;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
background-color: var(--bg-primary);
|
||
}
|
||
|
||
.upload-bar {
|
||
height: 100%;
|
||
width: 0%;
|
||
background-color: var(--color-primary);
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.receipts-container {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.receipt-thumbnail {
|
||
position: relative;
|
||
width: 100px;
|
||
height: 100px;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.receipt-thumbnail img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.receipt-thumbnail .overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0;
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.receipt-thumbnail:hover .overlay {
|
||
opacity: 1;
|
||
}
|
||
|
||
.receipt-thumbnail .overlay-btn {
|
||
background: white;
|
||
color: var(--color-text);
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
margin: 0 5px;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
z-index: 999;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.7);
|
||
overflow: auto;
|
||
}
|
||
|
||
.modal-content {
|
||
position: relative;
|
||
margin: auto;
|
||
padding: 0;
|
||
width: 80%;
|
||
max-width: 900px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.modal-close {
|
||
position: absolute;
|
||
top: 15px;
|
||
right: 15px;
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
color: white;
|
||
cursor: pointer;
|
||
z-index: 1000;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-img {
|
||
width: 100%;
|
||
height: auto;
|
||
display: block;
|
||
max-height: 80vh;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.modal-nav {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
padding: 16px;
|
||
color: white;
|
||
font-weight: bold;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 50px;
|
||
height: 50px;
|
||
user-select: none;
|
||
}
|
||
|
||
.modal-prev {
|
||
left: 15px;
|
||
}
|
||
|
||
.modal-next {
|
||
right: 15px;
|
||
}
|
||
|
||
.receipts-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
</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>
|
||
|
||
<!-- Receipt Upload Section -->
|
||
<div class="form-group">
|
||
<label>Receipts</label>
|
||
<div class="file-upload" id="receipt-upload">
|
||
<div class="file-upload-icon">📄</div>
|
||
<p>Drag & drop receipts here or click to browse</p>
|
||
<p class="file-size">(JPG, PNG, PDF up to 5MB each)</p>
|
||
<input type="file" id="receipt-input" multiple accept="image/jpeg,image/png,application/pdf">
|
||
<div class="upload-progress">
|
||
<div class="upload-bar" id="upload-progress-bar"></div>
|
||
</div>
|
||
</div>
|
||
<ul class="file-list" id="receipt-file-list"></ul>
|
||
</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>
|
||
|
||
<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>Receipts</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>
|
||
|
||
<!-- Modal for image preview -->
|
||
<div id="receipt-modal" class="modal">
|
||
<span class="modal-close" id="modal-close">×</span>
|
||
<div class="modal-content">
|
||
<img id="modal-img" class="modal-img" src="">
|
||
</div>
|
||
<span class="modal-nav modal-prev" id="modal-prev">❮</span>
|
||
<span class="modal-nav modal-next" id="modal-next">❯</span>
|
||
</div>
|
||
|
||
<script>
|
||
// Global variables
|
||
let apiUrl = '';
|
||
let apiToken = '';
|
||
let groupPayments = [];
|
||
let individualPayments = [];
|
||
let uploadedFiles = []; // Array to store uploaded file info
|
||
|
||
// 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');
|
||
|
||
// File upload elements
|
||
const receiptInput = document.getElementById('receipt-input');
|
||
const receiptFileList = document.getElementById('receipt-file-list');
|
||
const uploadProgressBar = document.getElementById('upload-progress-bar');
|
||
|
||
// Modal elements
|
||
const receiptModal = document.getElementById('receipt-modal');
|
||
const modalImg = document.getElementById('modal-img');
|
||
const modalClose = document.getElementById('modal-close');
|
||
const modalPrev = document.getElementById('modal-prev');
|
||
const modalNext = document.getElementById('modal-next');
|
||
let currentImgIndex = 0;
|
||
|
||
// 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);
|
||
|
||
receiptInput.addEventListener('change', handleFileSelect);
|
||
modalClose.addEventListener('click', closeModal);
|
||
modalPrev.addEventListener('click', showPrevImage);
|
||
modalNext.addEventListener('click', showNextImage);
|
||
|
||
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="6" style="text-align: center;">No group payments found</td>';
|
||
groupPaymentsList.appendChild(row);
|
||
return;
|
||
}
|
||
|
||
// Update table headers to include receipts column
|
||
const tableHeaders = document.querySelector('#group-payments-table thead tr');
|
||
if (tableHeaders && !tableHeaders.querySelector('th:nth-child(5)')) {
|
||
const receiptsHeader = document.createElement('th');
|
||
receiptsHeader.textContent = 'Receipts';
|
||
tableHeaders.insertBefore(receiptsHeader, tableHeaders.querySelector('th:last-child'));
|
||
}
|
||
|
||
groupPayments.forEach(payment => {
|
||
const row = document.createElement('tr');
|
||
|
||
// Create receipts cell
|
||
const receiptsCell = document.createElement('td');
|
||
const hasReceipts = payment.Receipts && payment.Receipts.length > 0;
|
||
|
||
if (hasReceipts) {
|
||
const receiptCount = payment.Receipts.length;
|
||
const receiptContainerId = `receipts-${payment.id}`;
|
||
|
||
receiptsCell.innerHTML = `
|
||
<div class="receipts-title">
|
||
<span>${receiptCount} receipt${receiptCount !== 1 ? 's' : ''}</span>
|
||
<button class="btn btn-sm" onclick="toggleReceiptView('${receiptContainerId}')">View</button>
|
||
</div>
|
||
<div id="${receiptContainerId}" class="receipts-container hidden"></div>
|
||
`;
|
||
} else {
|
||
receiptsCell.textContent = 'None';
|
||
}
|
||
|
||
row.innerHTML = `
|
||
<td>${payment.Name || '-'}</td>
|
||
<td>€${parseFloat(payment.Money).toFixed(2)}</td>
|
||
<td>${payment['Paid overall'] ? '✅' : '❌'}</td>
|
||
<td>${payment.Notes || '-'}</td>
|
||
`;
|
||
|
||
row.appendChild(receiptsCell);
|
||
|
||
const actionsCell = document.createElement('td');
|
||
actionsCell.className = 'actions';
|
||
actionsCell.innerHTML = `
|
||
<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>
|
||
`;
|
||
|
||
row.appendChild(actionsCell);
|
||
groupPaymentsList.appendChild(row);
|
||
|
||
// Render receipts if they exist
|
||
if (hasReceipts) {
|
||
renderReceipts(payment.Receipts, `receipts-${payment.id}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Prepare receipts data
|
||
const receiptFiles = uploadedFiles.map(file => ({
|
||
name: file.name
|
||
}));
|
||
|
||
const paymentData = {
|
||
"Name": name,
|
||
"Money": amount,
|
||
"Notes": notes,
|
||
"Paid overall": paid,
|
||
"Receipts": receiptFiles
|
||
};
|
||
|
||
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();
|
||
|
||
// Reset uploaded files
|
||
uploadedFiles = [];
|
||
receiptFileList.innerHTML = '';
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
// Handle file selection
|
||
function handleFileSelect(event) {
|
||
const files = event.target.files;
|
||
if (!files || files.length === 0) return;
|
||
|
||
// Process each file
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
|
||
// Validate file type and size
|
||
if (!validateFile(file)) continue;
|
||
|
||
// Add to UI
|
||
addFileToUI(file);
|
||
|
||
// Upload the file
|
||
uploadFile(file);
|
||
}
|
||
}
|
||
|
||
// Validate file (type and size)
|
||
function validateFile(file) {
|
||
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
|
||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||
|
||
if (!allowedTypes.includes(file.type)) {
|
||
showAlert('Only JPG, PNG and PDF files are allowed', 'danger');
|
||
return false;
|
||
}
|
||
|
||
if (file.size > maxSize) {
|
||
showAlert(`File ${file.name} exceeds the 5MB limit`, 'danger');
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// Add file to UI list
|
||
function addFileToUI(file) {
|
||
const fileId = `file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||
const fileItem = document.createElement('li');
|
||
fileItem.id = fileId;
|
||
fileItem.className = 'file-item';
|
||
fileItem.innerHTML = `
|
||
<div class="file-info">
|
||
<span class="file-name">${file.name}</span>
|
||
<span class="file-size">${formatFileSize(file.size)}</span>
|
||
</div>
|
||
<div class="file-actions">
|
||
<span class="upload-status">Uploading...</span>
|
||
</div>
|
||
`;
|
||
|
||
receiptFileList.appendChild(fileItem);
|
||
return fileId;
|
||
}
|
||
|
||
// Format file size
|
||
function formatFileSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' bytes';
|
||
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||
else return (bytes / 1048576).toFixed(1) + ' MB';
|
||
}
|
||
|
||
// Upload file to server
|
||
async function uploadFile(file) {
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const response = await fetch(`${apiUrl}/api/user-files/upload-file/`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Token ${apiToken}`
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json();
|
||
throw new Error(errorData.description || response.statusText);
|
||
}
|
||
|
||
const fileData = await response.json();
|
||
uploadedFiles.push(fileData);
|
||
|
||
// Update UI to show upload success
|
||
updateFileUI(file.name, fileData);
|
||
showAlert(`Successfully uploaded ${file.name}`, 'success');
|
||
|
||
} catch (error) {
|
||
showAlert(`Failed to upload ${file.name}: ${error.message}`, 'danger');
|
||
updateFileUI(file.name, null, true);
|
||
}
|
||
}
|
||
|
||
// Update file UI after upload
|
||
function updateFileUI(fileName, fileData, isError = false) {
|
||
const fileItems = receiptFileList.querySelectorAll('.file-item');
|
||
|
||
for (let item of fileItems) {
|
||
const nameElem = item.querySelector('.file-name');
|
||
|
||
if (nameElem && nameElem.textContent === fileName) {
|
||
const actionsElem = item.querySelector('.file-actions');
|
||
|
||
if (isError) {
|
||
actionsElem.innerHTML = `
|
||
<span class="upload-status" style="color: var(--color-danger);">Failed</span>
|
||
<button type="button" class="file-remove-btn" onclick="removeFile('${item.id}', null)">🗑️</button>
|
||
`;
|
||
} else {
|
||
const isImage = fileData.is_image;
|
||
actionsElem.innerHTML = `
|
||
<span class="upload-status" style="color: var(--color-success);">Uploaded</span>
|
||
${isImage ? `<button type="button" class="file-preview-btn" onclick="previewFile('${fileData.url}')">👁️</button>` : ''}
|
||
<button type="button" class="file-remove-btn" onclick="removeFile('${item.id}', '${fileData.name}')">🗑️</button>
|
||
`;
|
||
|
||
// Store file data in the element for later use
|
||
item.dataset.fileData = JSON.stringify(fileData);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Remove file from UI and uploaded files array
|
||
function removeFile(fileId, fileName) {
|
||
const fileItem = document.getElementById(fileId);
|
||
if (fileItem) {
|
||
if (fileName) {
|
||
// Find and remove from uploadedFiles array
|
||
uploadedFiles = uploadedFiles.filter(file => file.name !== fileName);
|
||
}
|
||
fileItem.remove();
|
||
}
|
||
}
|
||
|
||
// Preview image file
|
||
function previewFile(url) {
|
||
modalImg.src = url;
|
||
receiptModal.style.display = 'block';
|
||
|
||
// Set current index for navigation
|
||
const imageUrls = uploadedFiles
|
||
.filter(file => file.is_image)
|
||
.map(file => file.url);
|
||
|
||
currentImgIndex = imageUrls.indexOf(url);
|
||
}
|
||
|
||
// Close modal
|
||
function closeModal() {
|
||
receiptModal.style.display = 'none';
|
||
}
|
||
|
||
// Show previous image
|
||
function showPrevImage() {
|
||
const imageUrls = uploadedFiles
|
||
.filter(file => file.is_image)
|
||
.map(file => file.url);
|
||
|
||
if (imageUrls.length <= 1) return;
|
||
|
||
currentImgIndex = (currentImgIndex - 1 + imageUrls.length) % imageUrls.length;
|
||
modalImg.src = imageUrls[currentImgIndex];
|
||
}
|
||
|
||
// Show next image
|
||
function showNextImage() {
|
||
const imageUrls = uploadedFiles
|
||
.filter(file => file.is_image)
|
||
.map(file => file.url);
|
||
|
||
if (imageUrls.length <= 1) return;
|
||
|
||
currentImgIndex = (currentImgIndex + 1) % imageUrls.length;
|
||
modalImg.src = imageUrls[currentImgIndex];
|
||
}
|
||
|
||
// Render receipts for a group payment
|
||
function renderReceipts(receipts, containerId) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
if (!receipts || receipts.length === 0) {
|
||
container.innerHTML = '<p>No receipts attached</p>';
|
||
return;
|
||
}
|
||
|
||
receipts.forEach((receipt, index) => {
|
||
if (receipt.is_image) {
|
||
const thumbnailDiv = document.createElement('div');
|
||
thumbnailDiv.className = 'receipt-thumbnail';
|
||
|
||
const img = document.createElement('img');
|
||
img.src = receipt.thumbnails?.small?.url || receipt.url;
|
||
img.alt = 'Receipt';
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'overlay';
|
||
|
||
const viewBtn = document.createElement('button');
|
||
viewBtn.className = 'overlay-btn';
|
||
viewBtn.innerHTML = '👁️';
|
||
viewBtn.onclick = () => {
|
||
viewGroupReceipt(receipts, index);
|
||
};
|
||
|
||
overlay.appendChild(viewBtn);
|
||
thumbnailDiv.appendChild(img);
|
||
thumbnailDiv.appendChild(overlay);
|
||
container.appendChild(thumbnailDiv);
|
||
} else {
|
||
const linkDiv = document.createElement('div');
|
||
linkDiv.className = 'receipt-thumbnail';
|
||
linkDiv.style.backgroundColor = 'var(--bg-tertiary)';
|
||
linkDiv.style.display = 'flex';
|
||
linkDiv.style.alignItems = 'center';
|
||
linkDiv.style.justifyContent = 'center';
|
||
|
||
const linkIcon = document.createElement('div');
|
||
linkIcon.textContent = '📄';
|
||
linkIcon.style.fontSize = '2em';
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'overlay';
|
||
|
||
const linkBtn = document.createElement('button');
|
||
linkBtn.className = 'overlay-btn';
|
||
linkBtn.innerHTML = '↗️';
|
||
linkBtn.onclick = () => {
|
||
window.open(receipt.url, '_blank');
|
||
};
|
||
|
||
overlay.appendChild(linkBtn);
|
||
linkDiv.appendChild(linkIcon);
|
||
linkDiv.appendChild(overlay);
|
||
container.appendChild(linkDiv);
|
||
}
|
||
});
|
||
}
|
||
|
||
// View a receipt from group payments
|
||
function viewGroupReceipt(receipts, index) {
|
||
const imageReceipts = receipts.filter(r => r.is_image);
|
||
if (imageReceipts.length === 0) return;
|
||
|
||
modalImg.src = imageReceipts[index].url;
|
||
receiptModal.style.display = 'block';
|
||
currentImgIndex = index;
|
||
|
||
// Update navigation for group receipts
|
||
modalPrev.onclick = () => {
|
||
index = (index - 1 + imageReceipts.length) % imageReceipts.length;
|
||
modalImg.src = imageReceipts[index].url;
|
||
currentImgIndex = index;
|
||
};
|
||
|
||
modalNext.onclick = () => {
|
||
index = (index + 1) % imageReceipts.length;
|
||
modalImg.src = imageReceipts[index].url;
|
||
currentImgIndex = index;
|
||
};
|
||
}
|
||
|
||
// Make sure we prevent default drag behaviors
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const dropZone = document.getElementById('receipt-upload');
|
||
|
||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||
dropZone.addEventListener(eventName, preventDefaults, false);
|
||
});
|
||
|
||
function preventDefaults(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
|
||
['dragenter', 'dragover'].forEach(eventName => {
|
||
dropZone.addEventListener(eventName, highlight, false);
|
||
});
|
||
|
||
['dragleave', 'drop'].forEach(eventName => {
|
||
dropZone.addEventListener(eventName, unhighlight, false);
|
||
});
|
||
|
||
function highlight() {
|
||
dropZone.classList.add('file-upload-active');
|
||
}
|
||
|
||
function unhighlight() {
|
||
dropZone.classList.remove('file-upload-active');
|
||
}
|
||
|
||
dropZone.addEventListener('drop', handleDrop, false);
|
||
|
||
function handleDrop(e) {
|
||
const dt = e.dataTransfer;
|
||
const files = dt.files;
|
||
|
||
if (files && files.length > 0) {
|
||
// Convert FileList to array and process
|
||
const fileArray = Array.from(files);
|
||
fileArray.forEach(file => {
|
||
if (validateFile(file)) {
|
||
addFileToUI(file);
|
||
uploadFile(file);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
// Toggle receipt view
|
||
function toggleReceiptView(containerId) {
|
||
const container = document.getElementById(containerId);
|
||
if (container) {
|
||
container.classList.toggle('hidden');
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
setInitialTheme();
|
||
renderUsers();
|
||
onPaymentModeChange();
|
||
checkSavedCredentials();
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html> |