feat: add member details popup with attendance and fee exceptions
This commit is contained in:
@@ -183,6 +183,166 @@
|
||||
border-bottom: 1px solid #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
color: #00ff00;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
width: 250px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.filter-input:focus {
|
||||
border-color: #00ff00;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
color: #888;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
color: #00ff00;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
#memberModal {
|
||||
display: none !important;
|
||||
/* Force hide by default */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
z-index: 9999;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#memberModal.active {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #0c0c0c;
|
||||
border: 1px solid #00ff00;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px rgba(0, 255, 0, 0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #333;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
color: #00ff00;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
color: #ff3333;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.modal-section {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.modal-section-title {
|
||||
color: #555;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px dashed #222;
|
||||
}
|
||||
|
||||
.modal-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.modal-table th,
|
||||
.modal-table td {
|
||||
text-align: left;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-table th {
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tx-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tx-item {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px dashed #222;
|
||||
}
|
||||
|
||||
.tx-meta {
|
||||
color: #555;
|
||||
font-size: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tx-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.tx-amount {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.tx-sender {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.tx-msg {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -201,6 +361,11 @@
|
||||
<a href="{{ payments_url }}" target="_blank">Payments Ledger</a>
|
||||
</div>
|
||||
|
||||
<div class="filter-container">
|
||||
<span class="filter-label">search member:</span>
|
||||
<input type="text" id="nameFilter" class="filter-input" placeholder="..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -212,10 +377,13 @@
|
||||
<th>Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="reconcileBody">
|
||||
{% for row in results %}
|
||||
<tr>
|
||||
<td>{{ row.name }}</td>
|
||||
<tr class="member-row">
|
||||
<td class="member-name">
|
||||
{{ row.name }}
|
||||
<span class="info-icon" onclick="showMemberDetails('{{ row.name|e }}')">[i]</span>
|
||||
</td>
|
||||
{% for cell in row.months %}
|
||||
<td
|
||||
class="{% if cell == '-' %}cell-empty{% elif 'UNPAID' in cell %}cell-unpaid{% elif cell == 'OK' %}cell-ok{% endif %}">
|
||||
@@ -275,6 +443,183 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="memberModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="modalMemberName">Member Name</div>
|
||||
<div class="close-btn" onclick="closeModal()">[close]</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Status Summary</div>
|
||||
<div id="modalTier" style="margin-bottom: 10px; color: #888;">Tier: -</div>
|
||||
<table class="modal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Month</th>
|
||||
<th style="text-align: center;">Att.</th>
|
||||
<th style="text-align: center;">Expected</th>
|
||||
<th style="text-align: center;">Paid</th>
|
||||
<th style="text-align: right;">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="modalStatusBody">
|
||||
<!-- Filled by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-section" id="modalExceptionSection" style="display: none;">
|
||||
<div class="modal-section-title">Fee Exceptions</div>
|
||||
<div id="modalExceptionList" class="tx-list">
|
||||
<!-- Filled by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-section">
|
||||
<div class="modal-section-title">Payment History</div>
|
||||
<div id="modalTxList" class="tx-list">
|
||||
<!-- Filled by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const memberData = {{ member_data| safe }};
|
||||
const sortedMonths = {{ raw_months| tojson }};
|
||||
|
||||
function showMemberDetails(name) {
|
||||
const data = memberData[name];
|
||||
if (!data) return;
|
||||
|
||||
document.getElementById('modalMemberName').textContent = name;
|
||||
document.getElementById('modalTier').textContent = 'Tier: ' + data.tier;
|
||||
|
||||
const statusBody = document.getElementById('modalStatusBody');
|
||||
statusBody.innerHTML = '';
|
||||
|
||||
// Collect all transactions for listing
|
||||
const allTransactions = [];
|
||||
|
||||
// We need to iterate over months in reverse to show newest first
|
||||
const monthKeys = Object.keys(data.months).sort().reverse();
|
||||
|
||||
monthKeys.forEach(m => {
|
||||
const mdata = data.months[m];
|
||||
const expected = mdata.expected || 0;
|
||||
const paid = mdata.paid || 0;
|
||||
const attendance = mdata.attendance_count || 0;
|
||||
const originalExpected = mdata.original_expected;
|
||||
|
||||
let status = '-';
|
||||
let statusClass = '';
|
||||
if (expected > 0 || paid > 0) {
|
||||
if (paid >= expected && expected > 0) { status = 'OK'; statusClass = 'cell-ok'; }
|
||||
else if (paid > 0) { status = paid + '/' + expected; }
|
||||
else { status = 'UNPAID ' + expected; statusClass = 'cell-unpaid'; }
|
||||
}
|
||||
|
||||
const expectedCell = mdata.exception
|
||||
? `<span style="color: #ffaa00;" title="Overridden from ${originalExpected}">${expected}*</span>`
|
||||
: expected;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td style="color: #888;">${m}</td>
|
||||
<td style="text-align: center; color: #ccc;">${attendance}</td>
|
||||
<td style="text-align: center; color: #ccc;">${expectedCell}</td>
|
||||
<td style="text-align: center; color: #ccc;">${paid}</td>
|
||||
<td style="text-align: right;" class="${statusClass}">${status}</td>
|
||||
`;
|
||||
statusBody.appendChild(row);
|
||||
|
||||
if (mdata.transactions) {
|
||||
mdata.transactions.forEach(tx => {
|
||||
allTransactions.push({ month: m, ...tx });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const exList = document.getElementById('modalExceptionList');
|
||||
const exSection = document.getElementById('modalExceptionSection');
|
||||
exList.innerHTML = '';
|
||||
|
||||
const exceptions = [];
|
||||
monthKeys.forEach(m => {
|
||||
if (data.months[m].exception) {
|
||||
exceptions.push({ month: m, ...data.months[m].exception });
|
||||
}
|
||||
});
|
||||
|
||||
if (exceptions.length > 0) {
|
||||
exSection.style.display = 'block';
|
||||
exceptions.forEach(ex => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'tx-item'; // Reuse style
|
||||
item.innerHTML = `
|
||||
<div class="tx-meta">${ex.month}</div>
|
||||
<div class="tx-main">
|
||||
<span class="tx-amount" style="color: #ffaa00;">${ex.amount} CZK</span>
|
||||
</div>
|
||||
<div class="tx-msg">${ex.note || 'No details provided.'}</div>
|
||||
`;
|
||||
exList.appendChild(item);
|
||||
});
|
||||
} else {
|
||||
exSection.style.display = 'none';
|
||||
}
|
||||
|
||||
const txList = document.getElementById('modalTxList');
|
||||
txList.innerHTML = '';
|
||||
|
||||
if (allTransactions.length === 0) {
|
||||
txList.innerHTML = '<div style="color: #444; font-style: italic; padding: 10px 0;">No transactions matched to this member.</div>';
|
||||
} else {
|
||||
allTransactions.sort((a, b) => b.date.localeCompare(a.date)).forEach(tx => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'tx-item';
|
||||
item.innerHTML = `
|
||||
<div class="tx-meta">${tx.date} | matched to ${tx.month}</div>
|
||||
<div class="tx-main">
|
||||
<span class="tx-amount">${tx.amount} CZK</span>
|
||||
<span class="tx-sender">${tx.sender}</span>
|
||||
</div>
|
||||
<div class="tx-msg">${tx.message || ''}</div>
|
||||
`;
|
||||
txList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('memberModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('memberModal').classList.remove('active');
|
||||
}
|
||||
|
||||
// Existing filter script
|
||||
document.getElementById('nameFilter').addEventListener('input', function (e) {
|
||||
const filterValue = e.target.value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||
const rows = document.querySelectorAll('.member-row');
|
||||
|
||||
rows.forEach(row => {
|
||||
const nameNode = row.querySelector('.member-name');
|
||||
const name = nameNode.childNodes[0].textContent.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
||||
if (name.includes(filterValue)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close on Esc
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
```
|
||||
Reference in New Issue
Block a user