fix event charge and invoice update

This commit is contained in:
root
2026-04-18 09:54:29 -04:00
parent a448288878
commit dd1d492c30
16 changed files with 1078 additions and 89 deletions
+398 -14
View File
@@ -73,13 +73,15 @@
<div class="card-body">
<form id="event-participant-form" action="<?= site_url('payment/event_charges') ?>" method="post">
<?= csrf_field() ?>
<input type="hidden" name="school_year" value="<?= esc($school_year) ?>">
<input type="hidden" name="semester" value="<?= esc($semester) ?>">
<div class="row g-3">
<div class="col-md-4">
<label for="event_id" class="form-label">Select Event</label>
<select name="event_id" id="event_id" class="form-select" required>
<option value="">-- All events --</option>
<?php foreach ($events as $event): ?>
<option value="<?= esc($event['id']) ?>" <?= isset($filterEventId) && $filterEventId == $event['id'] ? 'selected' : '' ?>>
<option value="<?= esc($event['id']) ?>" data-amount="<?= esc(number_format($event['amount'] ?? 0, 2, '.', '')) ?>" <?= isset($filterEventId) && $filterEventId == $event['id'] ? 'selected' : '' ?>>
<?= esc($event['event_name']) ?>
</option>
<?php endforeach; ?>
@@ -109,7 +111,7 @@
<div class="col-md-4">
<label for="parent_id" class="form-label">Parents List</label>
<select id="parent_id" name="parent_id" class="form-select" required>
<select id="parent_id" name="parent_id" class="form-select">
<option value="">-- Select Parent --</option>
<?php foreach ($parents as $parent): ?>
<option value="<?= esc($parent['id']) ?>" <?= $filterParentId && $filterParentId == $parent['id'] ? 'selected' : '' ?>>
@@ -125,6 +127,14 @@
<div id="studentList" class="row g-3"></div>
</div>
<div class="mt-4">
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#externalParticipantModal">
Add non-school participant
</button>
<div class="form-text">After adding participants, click Submit to save them to the database.</div>
<div id="externalParticipantsHidden"></div>
</div>
<div class="d-flex flex-wrap gap-2 mt-3">
<button type="submit" class="btn btn-primary">Submit</button>
<a href="<?= site_url('administrator/events') ?>" class="btn btn-success">Event List</a>
@@ -136,16 +146,24 @@
<?php
$grouped = [];
foreach ($charges as $charge) {
$label = $charge['event_name'] ?? 'N/A';
$grouped[$label][] = $charge;
$eventId = (int)($charge['event_id'] ?? 0);
if (!isset($grouped[$eventId])) {
$grouped[$eventId] = [
'label' => $charge['event_name'] ?? 'N/A',
'rows' => [],
];
}
$grouped[$eventId]['rows'][] = $charge;
}
?>
<?php if (empty($grouped)): ?>
<div class="alert alert-info">No charges found.</div>
<?php else: ?>
<?php foreach ($grouped as $eventLabel => $rows): ?>
<div class="card mb-4">
<?php foreach ($grouped as $eventId => $data): ?>
<?php $eventLabel = $data['label']; ?>
<?php $rows = $data['rows']; ?>
<div class="card mb-4 event-card" data-event-id="<?= esc($eventId) ?>">
<div class="card-header bg-light fw-semibold">
<?= esc($eventLabel) ?>
</div>
@@ -155,12 +173,13 @@
$totalCharged = 0.0;
?>
<div class="table-responsive">
<table class="table table-bordered table-striped mb-0 no-mgmt-sticky">
<table id="eventTable_<?= esc($eventId) ?>" class="table table-bordered table-striped mb-0 no-mgmt-sticky">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Parent Name</th>
<th>Student Name</th>
<th>External Info</th>
<th>Class Section</th>
<th>Charged Amount</th>
<th>Created</th>
@@ -177,11 +196,37 @@
?>
<tr>
<td><?= esc($charge['id']) ?></td>
<td><?= esc($charge['parent_firstname'] . ' ' . $charge['parent_lastname']) ?></td>
<?php
$studentName = $charge['student_firstname']
? trim($charge['student_firstname'] . ' ' . $charge['student_lastname'])
: '';
$externalName = trim(
($charge['external_firstname'] ?? '') . ' ' .
($charge['external_lastname'] ?? '')
);
$displayName = $studentName ?: ($externalName ?: '—');
$externalNote = $charge['external_note'] ?? '';
$externalParentLabel = trim(
($charge['external_parent_firstname'] ?? '') . ' ' .
($charge['external_parent_lastname'] ?? '')
);
$externalParentPhone = $charge['external_parent_phone'] ?? '';
$standardParentName = trim($charge['parent_firstname'] . ' ' . $charge['parent_lastname']);
$parentColumnName = $externalParentLabel ?: ($standardParentName ?: '—');
?>
<td><?= esc($parentColumnName) ?></td>
<td>
<?= $charge['student_firstname']
? esc($charge['student_firstname'] . ' ' . $charge['student_lastname'])
: '-' ?>
<?= esc($displayName) ?>
<?php if ($externalNote): ?>
<small class="text-muted d-block"><?= esc($externalNote) ?></small>
<?php endif; ?>
</td>
<td>
<?php if ($externalParentPhone): ?>
<div class="text-muted small"><?= esc($externalParentPhone) ?></div>
<?php else: ?>
<span class="text-muted small">—</span>
<?php endif; ?>
</td>
<td>
<?= esc($classSectionNames[$charge['class_section_id'] ?? ''] ?? '—') ?>
@@ -246,8 +291,63 @@
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div class="modal fade" id="externalParticipantModal" tabindex="-1" aria-labelledby="externalParticipantModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="externalParticipantForm" class="needs-validation" novalidate>
<?= csrf_field() ?>
<div class="modal-header">
<h5 class="modal-title" id="externalParticipantModalLabel">Add Non-School Participant</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="small text-muted">Provide the students name and the accompanying guardian/contact details.</p>
<div class="mb-3">
<label class="form-label">Student / Kid Info</label>
<div class="row g-3">
<div class="col-md-6">
<input type="text" class="form-control" id="modalExternalFirstName" placeholder="First name" required>
</div>
<div class="col-md-6">
<input type="text" class="form-control" id="modalExternalLastName" placeholder="Last name" required>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Parent / Contact Info</label>
<div class="row g-3">
<div class="col-md-6">
<input type="text" class="form-control" id="modalParentFirstName" placeholder="Parent first name" required>
</div>
<div class="col-md-6">
<input type="text" class="form-control" id="modalParentLastName" placeholder="Parent last name" required>
</div>
<div class="col-12">
<input type="text" class="form-control" id="modalParentPhone" placeholder="Parent phone or contact">
</div>
<div class="col-12">
<input type="email" class="form-control" id="modalParentEmail" placeholder="Parent email" required>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="modalExternalNote">Note <span class="text-muted small">(optional)</span></label>
<textarea rows="2" class="form-control" id="modalExternalNote" placeholder="Additional info (e.g., guest, sibling, etc.)"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="externalParticipantSave">
Add to list
</button>
</div>
</form>
</div>
</div>
</div>
<?= $this->endSection() ?>
@@ -295,7 +395,20 @@ function loadStudentsWithCharges() {
}
}
$(function() {
const capitalizeName = (value) => {
if (!value) return '';
return value
.split(' ')
.map(part => {
const trimmed = part.trim();
if (!trimmed) return '';
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1).toLowerCase();
})
.filter(Boolean)
.join(' ');
};
$(function() {
$('#parent_id').on('change', loadStudentsWithCharges);
$('#event_id').on('change', function() {
@@ -312,6 +425,277 @@ $(function() {
if ($('#event_id').val() && $('#parent_id').val()) {
loadStudentsWithCharges();
}
const $externalHidden = $('#externalParticipantsHidden');
const storageKey = 'eventChargesExternalParticipants';
let externalDrafts = [];
let editingKey = null;
const escapeHtml = (value) => {
const s = String(value ?? '');
return s.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
};
function buildExternalRow(entry) {
const key = entry.key;
const isPaid = !!entry.paid;
const firstEsc = escapeHtml(entry.firstname);
const lastEsc = escapeHtml(entry.lastname);
const noteEsc = escapeHtml(entry.note);
const parentFirstEsc = escapeHtml(entry.parentFirstname);
const parentLastEsc = escapeHtml(entry.parentLastname);
const parentPhoneEsc = escapeHtml(entry.parentPhone);
const parentEmailEsc = escapeHtml(entry.parentEmail);
const label = (firstEsc || lastEsc)
? `${firstEsc || ''} ${lastEsc || ''}`.trim()
: 'Unnamed participant';
const parentLabel = (parentFirstEsc || parentLastEsc)
? `<div class="text-muted small">Parent: ${parentFirstEsc} ${parentLastEsc}</div>`
: '';
const phoneLabel = parentPhoneEsc
? `<div class="text-muted small">${parentPhoneEsc}</div>`
: '';
const parentNameForRow = entry.parentDisplayName || '—';
const amountDisplay = ((entry.eventAmount ?? 0) || 0.0).toFixed(2);
const $wrapper = $(`
<div data-key="${key}">
<input type="hidden" name="external_participants[${key}][firstname]" value="${firstEsc}">
<input type="hidden" name="external_participants[${key}][lastname]" value="${lastEsc}">
<input type="hidden" name="external_participants[${key}][note]" value="${noteEsc}">
<input type="hidden" name="external_participants[${key}][parent_firstname]" value="${parentFirstEsc}">
<input type="hidden" name="external_participants[${key}][parent_lastname]" value="${parentLastEsc}">
<input type="hidden" name="external_participants[${key}][parent_phone]" value="${parentPhoneEsc}">
<input type="hidden" name="external_participants[${key}][parent_email]" value="${parentEmailEsc}">
<input type="hidden" name="external_participants[${key}][paid]" value="${isPaid ? '1' : '0'}">
</div>
`);
$externalHidden.append($wrapper);
const $tableBody = $(`#eventTable_${entry.eventId} tbody`);
if (!$tableBody.length) {
return;
}
const createdDisplay = new Date().toLocaleString();
const $previewRow = $(`
<tr class="external-preview table-warning" data-key="${key}" data-event-id="${entry.eventId}">
<td>—</td>
<td>${escapeHtml(parentNameForRow)}</td>
<td>
<strong>${label}</strong>
${noteEsc ? `<div class="text-muted small">${noteEsc}</div>` : ''}
</td>
<td>
${phoneLabel}
${phoneLabel ? '' : '<span class="text-muted small">—</span>'}
</td>
<td>External</td>
<td>$${amountDisplay}</td>
<td>${createdDisplay}</td>
<td class="text-center">
<span class="badge bg-warning text-dark">Pending</span>
${isPaid ? '<span class="badge bg-success ms-1 external-paid-badge">Paid on save</span>' : '<span class="badge bg-secondary ms-1 external-paid-badge d-none">Paid on save</span>'}
</td>
<td class="text-center">
<div class="form-check d-inline-flex align-items-center gap-2 justify-content-center m-0">
<input class="form-check-input external-paid-toggle" type="checkbox" data-key="${key}" ${isPaid ? 'checked' : ''}>
<span class="small text-muted">On save</span>
</div>
</td>
<td class="text-end d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm edit-external-participant" data-key="${key}">
Edit
</button>
<button type="button" class="btn btn-outline-danger btn-sm remove-external-participant" data-key="${key}">
Remove
</button>
</td>
</tr>
`);
$tableBody.append($previewRow);
}
function persistDrafts() {
localStorage.setItem(storageKey, JSON.stringify(externalDrafts));
}
function saveExternalDraft(entry) {
entry.key = entry.key ?? `${Date.now()}_${Math.random().toString(36).slice(2)}`;
entry.paid = !!entry.paid;
removeExternalEntryFromDom(entry.key);
externalDrafts = externalDrafts.filter((existing) => existing.key !== entry.key);
externalDrafts.push(entry);
persistDrafts();
buildExternalRow(entry);
}
function removeExternalEntryFromDom(key) {
$(`tr.external-preview[data-key="${key}"]`).remove();
$externalHidden.find(`div[data-key="${key}"]`).remove();
}
function removeDraft(key) {
externalDrafts = externalDrafts.filter((entry) => entry.key !== key);
persistDrafts();
}
function loadDrafts() {
const raw = localStorage.getItem(storageKey);
if (!raw) {
return;
}
try {
const saved = JSON.parse(raw);
if (!Array.isArray(saved)) {
return;
}
externalDrafts = saved;
const currentEvent = $('#event_id').val();
externalDrafts.forEach((entry) => {
if (String(entry.eventId) === String(currentEvent)) {
buildExternalRow(entry);
}
});
} catch (e) {
console.error('Failed to load external drafts', e);
}
}
function getDraftByKey(key) {
return externalDrafts.find((entry) => entry.key === key);
}
function openExternalModalForEntry(key) {
const entry = getDraftByKey(key);
if (!entry) {
return;
}
editingKey = key;
$('#modalExternalFirstName').val(entry.firstname);
$('#modalExternalLastName').val(entry.lastname);
$('#modalExternalNote').val(entry.note);
$('#modalParentFirstName').val(entry.parentFirstname);
$('#modalParentLastName').val(entry.parentLastname);
$('#modalParentPhone').val(entry.parentPhone);
$('#modalParentEmail').val(entry.parentEmail);
if (modalInstance) {
modalInstance.show();
}
}
const modalElement = document.getElementById('externalParticipantModal');
const modalInstance = modalElement ? new bootstrap.Modal(modalElement) : null;
if (modalElement) {
modalElement.addEventListener('hidden.bs.modal', () => {
editingKey = null;
});
}
function clearModalInputs() {
$('#modalExternalFirstName').val('');
$('#modalExternalLastName').val('');
$('#modalExternalNote').val('');
$('#modalParentFirstName').val('');
$('#modalParentLastName').val('');
$('#modalParentPhone').val('');
$('#modalParentEmail').val('');
}
$('#externalParticipantSave').on('click', function() {
const firstName = capitalizeName($('#modalExternalFirstName').val().trim());
const lastName = capitalizeName($('#modalExternalLastName').val().trim());
const note = $('#modalExternalNote').val().trim();
if (!firstName && !lastName) {
alert('Please provide at least a first or last name for the non-school participant.');
return;
}
const parentFirst = capitalizeName($('#modalParentFirstName').val().trim());
const parentLast = capitalizeName($('#modalParentLastName').val().trim());
const parentPhone = $('#modalParentPhone').val().trim();
const parentEmail = $('#modalParentEmail').val().trim();
const eventId = $('#event_id').val();
if (!eventId) {
alert('Select an event before adding a non-school participant.');
return;
}
const eventAmount = parseFloat($('#event_id option:selected').data('amount')) || 0;
const explicitParentName = `${parentFirst} ${parentLast}`.trim();
if (!explicitParentName) {
alert('Please enter the external kid parent name.');
return;
}
if (!parentEmail) {
alert('Please enter the external kid parent email.');
return;
}
const parentName = explicitParentName;
const draftEntry = editingKey ? getDraftByKey(editingKey) : null;
const entry = {
firstname: firstName,
lastname: lastName,
note: note,
parentFirstname: parentFirst,
parentLastname: parentLast,
parentPhone: parentPhone,
parentEmail: parentEmail,
eventId: (draftEntry && draftEntry.eventId) ? draftEntry.eventId : eventId,
eventAmount: (draftEntry && draftEntry.eventAmount) ? draftEntry.eventAmount : eventAmount,
parentDisplayName: (draftEntry && draftEntry.parentDisplayName) ? draftEntry.parentDisplayName : parentName,
paid: (draftEntry && typeof draftEntry.paid !== 'undefined') ? !!draftEntry.paid : false,
};
if (editingKey) {
entry.key = editingKey;
}
saveExternalDraft(entry);
editingKey = null;
clearModalInputs();
if (modalInstance) {
modalInstance.hide();
}
});
$(document).on('click', '.remove-external-participant', function() {
const $entry = $(this).closest('[data-key]');
const key = $entry.data('key');
removeExternalEntryFromDom(key);
if (key) {
removeDraft(key);
}
});
$(document).on('click', '.edit-external-participant', function() {
const key = $(this).data('key');
if (key) {
openExternalModalForEntry(key);
}
});
$(document).on('change', '.external-paid-toggle', function() {
const key = $(this).data('key');
const paid = !!this.checked;
if (!key) {
return;
}
const entry = getDraftByKey(key);
if (entry) {
entry.paid = paid;
persistDrafts();
}
$externalHidden
.find(`div[data-key="${key}"] input[name="external_participants[${key}][paid]"]`)
.val(paid ? '1' : '0');
const $badge = $(`tr.external-preview[data-key="${key}"] .external-paid-badge`);
if (paid) {
$badge.removeClass('d-none').addClass('bg-success').text('Paid on save');
} else {
$badge.addClass('d-none');
}
});
$('#event-participant-form').on('submit', function() {
localStorage.removeItem(storageKey);
});
loadDrafts();
});
</script>
<?= $this->endSection() ?>