Fix semester context, attendance rosters, and billing workflows
- load global semester helpers consistently and use date-based semester defaults - fix grading and daily attendance duplicate student/section rows - keep attendance violations scoped to the current semester by default - update invoice, refund, discount, payment, and financial aid flows - add configuration cleanup migrations for duplicate calendar/semester keys - refresh parent registration/report-card and print request handling - update related models, services, views, cron notes, and test coverage
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
<th>Year</th>
|
||||
<th>Status</th>
|
||||
<th>Requested</th>
|
||||
<th>Approved</th>
|
||||
<th>Submitted</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -40,12 +41,13 @@
|
||||
<td><?= esc($row['school_year'] ?? '') ?></td>
|
||||
<td><?= esc($row['status'] ?? '') ?></td>
|
||||
<td><?= $row['requested_amount'] !== null && $row['requested_amount'] !== '' ? '$' . number_format((float) $row['requested_amount'], 2) : '—' ?></td>
|
||||
<td><?= $row['admin_amount'] !== null && $row['admin_amount'] !== '' ? '$' . number_format((float) $row['admin_amount'], 2) : '—' ?></td>
|
||||
<td><?= esc($row['created_at'] ?? '') ?></td>
|
||||
<td><a class="btn btn-sm btn-outline-primary" href="<?= site_url('administrator/financial-aid/' . (int) $row['id']) ?>">Review</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($requests)): ?>
|
||||
<tr><td colspan="7" class="text-muted text-center">No financial aid requests found.</td></tr>
|
||||
<tr><td colspan="8" class="text-muted text-center">No financial aid requests found.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1>Apply Discount Voucher</h1>
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
|
||||
<form method="post" action="">
|
||||
<form method="post" action="<?= site_url('discount/apply') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<div class="d-flex gap-2 mb-3 justify-content-end">
|
||||
<div>
|
||||
|
||||
@@ -104,6 +104,18 @@
|
||||
if (!list || !list.length) return '<em>No students enrolled.</em>';
|
||||
return list.map(k => `${esc(k.name)} (Grade: ${esc(k.grade)})`).join('<br>');
|
||||
}
|
||||
function renderRefundCell(r) {
|
||||
const details = Array.isArray(r.refund_details) ? r.refund_details : [];
|
||||
const lines = details.map(d => {
|
||||
const ts = d.date ? Date.parse(d.date) : NaN;
|
||||
const date = Number.isNaN(ts) ? '-' : formatDateTime(ts);
|
||||
const method = d.method ? esc(d.method) : '-';
|
||||
const check = d.check_number ? `, Check # ${esc(d.check_number)}` : '';
|
||||
return `<div class="text-muted small">${date} · ${method}${check}</div>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="fw-semibold">${fmtMoney(r.refund_amount)}</div>${lines}`;
|
||||
}
|
||||
function renderParentCell(r) {
|
||||
const pid = parseInt(r.parent_id || 0, 10);
|
||||
const name = esc(r.parent_name || '');
|
||||
@@ -113,6 +125,21 @@
|
||||
}
|
||||
return name;
|
||||
}
|
||||
function renderInvoiceRow(r) {
|
||||
const ts = Date.parse(r.invoice_date || new Date().toISOString());
|
||||
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
|
||||
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
|
||||
|
||||
return [
|
||||
renderParentCell(r),
|
||||
renderStudents(r.enrolledKids || []),
|
||||
genBtn,
|
||||
fmtMoney(r.invoice_amount),
|
||||
renderRefundCell(r),
|
||||
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
|
||||
pdf,
|
||||
];
|
||||
}
|
||||
|
||||
async function generateInvoice(parentId) {
|
||||
const body = new URLSearchParams();
|
||||
@@ -160,20 +187,7 @@
|
||||
const resp = await loadInvoices(selectedSchoolYear());
|
||||
syncSchoolYearSelect(resp);
|
||||
|
||||
const data = (resp.invoices || []).map(r => {
|
||||
const ts = Date.parse(r.invoice_date || new Date().toISOString());
|
||||
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
|
||||
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
|
||||
return [
|
||||
renderParentCell(r),
|
||||
renderStudents(r.enrolledKids || []),
|
||||
genBtn,
|
||||
fmtMoney(r.invoice_amount),
|
||||
fmtMoney(r.refund_amount),
|
||||
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
|
||||
pdf,
|
||||
];
|
||||
});
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
if ($.fn.DataTable.isDataTable($tbl)) {
|
||||
const dti = $tbl.DataTable();
|
||||
dti.clear();
|
||||
@@ -190,20 +204,7 @@
|
||||
const resp = await loadInvoices();
|
||||
syncSchoolYearSelect(resp);
|
||||
|
||||
const data = (resp.invoices || []).map(r => {
|
||||
const ts = Date.parse(r.invoice_date || new Date().toISOString());
|
||||
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
|
||||
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
|
||||
return [
|
||||
renderParentCell(r),
|
||||
renderStudents(r.enrolledKids || []),
|
||||
genBtn,
|
||||
fmtMoney(r.invoice_amount),
|
||||
fmtMoney(r.refund_amount),
|
||||
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
|
||||
pdf,
|
||||
];
|
||||
});
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
|
||||
dt = $tbl.DataTable({
|
||||
data,
|
||||
@@ -232,20 +233,7 @@
|
||||
await generateInvoice(btn.getAttribute('data-parent-id'));
|
||||
// Refresh table minimally: reload data
|
||||
const resp = await loadInvoices(selectedSchoolYear());
|
||||
const data = (resp.invoices || []).map(r => {
|
||||
const ts = Date.parse(r.invoice_date || new Date().toISOString());
|
||||
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
|
||||
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
|
||||
return [
|
||||
renderParentCell(r),
|
||||
renderStudents(r.enrolledKids || []),
|
||||
genBtn,
|
||||
fmtMoney(r.invoice_amount),
|
||||
fmtMoney(r.refund_amount),
|
||||
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
|
||||
pdf,
|
||||
];
|
||||
});
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
if ($.fn.DataTable.isDataTable($tbl)) {
|
||||
const dti = $tbl.DataTable();
|
||||
dti.clear();
|
||||
@@ -265,20 +253,7 @@
|
||||
this.disabled = true;
|
||||
await generateInvoice(this.getAttribute('data-parent-id'));
|
||||
const resp = await loadInvoices(selectedSchoolYear());
|
||||
const data = (resp.invoices || []).map(r => {
|
||||
const ts = Date.parse(r.invoice_date || new Date().toISOString());
|
||||
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
|
||||
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
|
||||
return [
|
||||
esc(r.parent_name || ''),
|
||||
renderStudents(r.enrolledKids || []),
|
||||
genBtn,
|
||||
fmtMoney(r.invoice_amount),
|
||||
fmtMoney(r.refund_amount),
|
||||
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
|
||||
pdf,
|
||||
];
|
||||
});
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
if ($.fn.DataTable.isDataTable($tbl)) {
|
||||
const dti = $tbl.DataTable();
|
||||
dti.clear();
|
||||
@@ -307,20 +282,7 @@
|
||||
$year.addEventListener('change', async () => {
|
||||
try {
|
||||
const resp = await loadInvoices($year.value);
|
||||
const data = (resp.invoices || []).map(r => {
|
||||
const ts = Date.parse(r.invoice_date || new Date().toISOString());
|
||||
const genBtn = `<button type=\"button\" class=\"btn btn-primary btn-sm gen-invoice\" data-parent-id=\"${esc(r.parent_id)}\" onclick=\"return window.__genInvoice && window.__genInvoice(this)\">Generate Invoice</button>`;
|
||||
const pdf = r.invoice_id ? `<a href=\"<?= base_url('invoice/pdf') ?>/${esc(r.invoice_id)}\" target=\"_blank\" class=\"btn btn-info btn-sm external-link\">View/Print PDF</a>` : '<span class=\"text-muted\">No invoice yet</span>';
|
||||
return [
|
||||
esc(r.parent_name || ''),
|
||||
renderStudents(r.enrolledKids || []),
|
||||
genBtn,
|
||||
fmtMoney(r.invoice_amount),
|
||||
fmtMoney(r.refund_amount),
|
||||
`<span data-order=\"${ts}\">${formatDateTime(ts)}</span>`,
|
||||
pdf,
|
||||
];
|
||||
});
|
||||
const data = (resp.invoices || []).map(renderInvoiceRow);
|
||||
if ($.fn.DataTable.isDataTable($tbl)) {
|
||||
const dti = $tbl.DataTable();
|
||||
dti.clear();
|
||||
|
||||
@@ -233,7 +233,7 @@ html, body { overflow-x: hidden; }
|
||||
} catch (\Throwable $e) {
|
||||
$sy = (string)($cfg->getConfig('school_year') ?? '');
|
||||
}
|
||||
$sem = (string)($cfg->getConfig('semester') ?? '');
|
||||
$sem = (string)(getSemester() ?? '');
|
||||
$uid = (int)(session()->get('user_id') ?? 0);
|
||||
if ($uid) {
|
||||
$classOptions = $tcModel->getClassAssignmentsByUserId($uid, $sy, $sem);
|
||||
|
||||
@@ -73,6 +73,21 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.enrollment-withdraw-control {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: .5rem;
|
||||
justify-content: flex-end;
|
||||
min-width: max-content;
|
||||
padding-left: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.enrollment-withdraw-control .form-check-input {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.enrollment-modal-footer {
|
||||
gap: .5rem;
|
||||
}
|
||||
@@ -150,6 +165,18 @@
|
||||
.enrollment-status-table td > * {
|
||||
max-width: 58%;
|
||||
}
|
||||
|
||||
.enrollment-status-table td[data-label="Withdraw"] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.enrollment-status-table td[data-label="Withdraw"] > * {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.enrollment-status-table td[data-label="Withdraw"] .enrollment-withdraw-control {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -217,24 +244,6 @@ foreach (($students ?? []) as $student) {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<?php if ($familyFinancialSummary !== []): ?>
|
||||
<div class="border rounded p-3 mb-3 bg-light">
|
||||
<div class="fw-semibold mb-2">Family Account Information</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4"><span class="text-muted">Previous-year carry-over balance:</span> <strong data-financial-value="carry_over_balance"><?= esc($money($familyFinancialSummary['carry_over_balance'] ?? 0)) ?></strong></div>
|
||||
<div class="col-md-4"><span class="text-muted">Registration fee:</span> <strong data-financial-value="registration_fee"><?= esc($money($familyFinancialSummary['registration_fee'] ?? 0)) ?></strong></div>
|
||||
<div class="col-md-4"><span class="text-muted">Tuition due now:</span> <strong data-financial-value="tuition_due_at_registration"><?= esc($money($familyFinancialSummary['tuition_due_at_registration'] ?? 0)) ?></strong></div>
|
||||
<div class="col-md-4"><span class="text-muted">Mandatory fees:</span> <strong data-financial-value="mandatory_fees"><?= esc($money($familyFinancialSummary['mandatory_fees'] ?? 0)) ?></strong></div>
|
||||
<div class="col-md-4"><span class="text-muted">Current-year account balance:</span> <strong data-financial-value="current_balance"><?= esc($money($familyFinancialSummary['current_balance'] ?? 0)) ?></strong></div>
|
||||
<div class="col-md-4"><span class="text-muted">Total currently due:</span> <strong data-financial-value="amount_due"><?= esc($money($familyFinancialSummary['amount_due'] ?? 0)) ?></strong></div>
|
||||
</div>
|
||||
<?php if (!empty($familyFinancialSummary['policy_message'])): ?>
|
||||
<div class="small text-muted mt-2"><?= esc($familyFinancialSummary['policy_message']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="small mt-2"><a href="<?= site_url('parent/financial-aid') ?>">Request financial aid</a></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($students)): ?>
|
||||
<form action="<?= base_url('/parent/enroll_classes_handler') ?>" method="post" id="enrollmentFlowForm">
|
||||
<?= csrf_field() ?>
|
||||
@@ -297,7 +306,7 @@ foreach (($students ?? []) as $student) {
|
||||
<td data-label="Status"><?= $statusBadge($student['enrollment_status'] ?? '') ?></td>
|
||||
<td data-label="Withdraw">
|
||||
<?php if (($student['enrollment_status'] ?? '') === 'enrolled'): ?>
|
||||
<div class="form-check form-switch m-0">
|
||||
<div class="form-check form-switch m-0 enrollment-withdraw-control">
|
||||
<input class="form-check-input" type="checkbox" name="withdraw[]" value="<?= esc($student['id']) ?>" id="withdraw-<?= esc($student['id']) ?>" <?= !$isEditable ? 'disabled' : '' ?>>
|
||||
<label class="form-check-label small" for="withdraw-<?= esc($student['id']) ?>">Request</label>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,68 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
?>
|
||||
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
@media (max-width: 575.98px) {
|
||||
.parent-register-stack-table {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.parent-register-stack-table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.parent-register-stack-table tbody,
|
||||
.parent-register-stack-table tr,
|
||||
.parent-register-stack-table td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.parent-register-stack-table tr {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
margin-bottom: .85rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.parent-register-stack-table td {
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid #eef1f3;
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
justify-content: space-between;
|
||||
padding: .75rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.parent-register-stack-table td:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.parent-register-stack-table td::before {
|
||||
color: #6c757d;
|
||||
content: attr(data-label);
|
||||
flex: 0 0 42%;
|
||||
font-size: .8rem;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.parent-register-stack-table td > * {
|
||||
max-width: 58%;
|
||||
}
|
||||
|
||||
.parent-register-stack-table td[data-label="Action"] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.parent-register-stack-table td[data-label="Action"] .btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
<?= $this->section('content') ?>
|
||||
<div class="container my-5">
|
||||
<h3 class="text-center text-success mb-3" style="font-family: Arial, sans-serif;">Student Registration</h3>
|
||||
@@ -67,7 +129,7 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($existingKids)): ?>
|
||||
<h5>Student Information</h5>
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-bordered parent-register-stack-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>School ID</th>
|
||||
@@ -83,12 +145,12 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
<tbody>
|
||||
<?php foreach ($existingKids as $kid): ?>
|
||||
<tr>
|
||||
<td><?= esc($kid['school_id']) ?></td>
|
||||
<td><?= esc($kid['firstname']) ?></td>
|
||||
<td><?= esc($kid['lastname']) ?></td>
|
||||
<td><?= esc((new DateTime($kid['dob']))->format('m-d-Y')) ?></td>
|
||||
<td><?= esc($kid['registration_grade']) ?></td>
|
||||
<td>
|
||||
<td data-label="School ID"><?= esc($kid['school_id']) ?></td>
|
||||
<td data-label="First Name"><?= esc($kid['firstname']) ?></td>
|
||||
<td data-label="Last Name"><?= esc($kid['lastname']) ?></td>
|
||||
<td data-label="DOB"><?= esc((new DateTime($kid['dob']))->format('m-d-Y')) ?></td>
|
||||
<td data-label="Grade"><?= esc($kid['registration_grade']) ?></td>
|
||||
<td data-label="Medical Conditions">
|
||||
<?php
|
||||
$mc = is_array($kid['medical_conditions'] ?? null)
|
||||
? $kid['medical_conditions']
|
||||
@@ -99,7 +161,7 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
echo implode('<br>', array_map('esc', $mc));
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Allergies">
|
||||
<?php
|
||||
$al = is_array($kid['allergies'] ?? null)
|
||||
? $kid['allergies']
|
||||
@@ -109,7 +171,7 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
echo implode('<br>', array_map('esc', $al));
|
||||
?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<td class="text-center" data-label="Action">
|
||||
<?php if ($isEditable && !empty($kid['can_delete'])): ?>
|
||||
<form action="<?= base_url('/parent/delete_student/' . $kid['id']) ?>"
|
||||
method="post"
|
||||
@@ -141,7 +203,7 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
|
||||
<?php if (!empty($emergencies)): ?>
|
||||
<h5 class="mt-4">Emergency Contacts</h5>
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-bordered parent-register-stack-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>First Name</th>
|
||||
@@ -159,10 +221,10 @@ $disableEmergencyBtn = count($emergencies) >= $maxEmergency;
|
||||
$lastName = $nameParts[1] ?? '';
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($firstName) ?></td>
|
||||
<td><?= esc($lastName) ?></td>
|
||||
<td><?= esc($contact['cellphone']) ?></td>
|
||||
<td><?= esc($contact['relation']) ?></td>
|
||||
<td data-label="First Name"><?= esc($firstName) ?></td>
|
||||
<td data-label="Last Name"><?= esc($lastName) ?></td>
|
||||
<td data-label="Phone"><?= esc($contact['cellphone']) ?></td>
|
||||
<td data-label="Relation"><?= esc($contact['relation']) ?></td>
|
||||
</tr>
|
||||
<?php include(APPPATH . 'Views/parent/edit_emergency_contact.php'); ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -1,4 +1,81 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $this->section('styles') ?>
|
||||
<style>
|
||||
@media (max-width: 575.98px) {
|
||||
.parent-report-cards-table {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.parent-report-cards-table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.parent-report-cards-table tbody,
|
||||
.parent-report-cards-table tr,
|
||||
.parent-report-cards-table td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.parent-report-cards-table tr {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
margin-bottom: .85rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td {
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid #eef1f3;
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
justify-content: space-between;
|
||||
padding: .75rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td::before {
|
||||
color: #6c757d;
|
||||
content: attr(data-label);
|
||||
flex: 0 0 38%;
|
||||
font-size: .8rem;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td > * {
|
||||
max-width: 62%;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td[data-label="Action"] {
|
||||
display: block;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td[data-label="Action"]::before {
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td[data-label="Action"] > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.parent-report-cards-table td[data-label="Action"] form {
|
||||
align-items: stretch !important;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
margin-left: 0 !important;
|
||||
margin-top: .5rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
$isEditable = (bool) ($isEditable ?? true);
|
||||
@@ -30,7 +107,7 @@
|
||||
<div class="alert alert-info">No students available for report cards.</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered align-middle">
|
||||
<table class="table table-striped table-bordered align-middle parent-report-cards-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Student</th>
|
||||
@@ -51,10 +128,10 @@
|
||||
$hasReport = !empty(($reportAvailableMap ?? [])[$sid]);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td>
|
||||
<td><?= esc($student['class_section_name'] ?? 'N/A') ?></td>
|
||||
<td><?= $viewedAt ? esc(local_datetime($viewedAt, 'm-d-Y H:i')) : 'Not viewed' ?></td>
|
||||
<td>
|
||||
<td data-label="Student"><?= esc(trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''))) ?></td>
|
||||
<td data-label="Class Section"><?= esc($student['class_section_name'] ?? 'N/A') ?></td>
|
||||
<td data-label="Viewed"><?= $viewedAt ? esc(local_datetime($viewedAt, 'm-d-Y H:i')) : 'Not viewed' ?></td>
|
||||
<td data-label="Signature">
|
||||
<?php if ($signedAt): ?>
|
||||
<?= esc($signedName ?: 'Signed') ?><br>
|
||||
<small class="text-muted"><?= esc(local_datetime($signedAt, 'm-d-Y H:i')) ?></small>
|
||||
@@ -62,7 +139,7 @@
|
||||
<span class="text-muted">Not signed</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<td class="text-end" data-label="Action">
|
||||
<?php if ($hasReport): ?>
|
||||
<a class="btn btn-sm btn-outline-primary" target="_blank" href="<?= base_url('parent/report-cards/view/' . $sid) ?>">View Report</a>
|
||||
<?php else: ?>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// - Reset link uses '?' to clear query string reliably.
|
||||
|
||||
// Ensure helper functions are available
|
||||
if (function_exists('helper')) { @helper('GlobalConfigHelper'); }
|
||||
if (function_exists('helper')) { @helper('global_config'); }
|
||||
|
||||
// Resolve defaults
|
||||
$currentYear = function_exists('getSchoolYear') ? (string) (getSchoolYear() ?? '') : '';
|
||||
@@ -16,7 +16,7 @@ if ($currentYear === '' || $currentSem === '') {
|
||||
try {
|
||||
$cfg = new \App\Models\ConfigurationModel();
|
||||
if ($currentYear === '') $currentYear = (string) ($cfg->getConfig('school_year') ?? '');
|
||||
if ($currentSem === '') $currentSem = (string) ($cfg->getConfig('semester') ?? '');
|
||||
if ($currentSem === '') $currentSem = (string) (getSemester() ?? '');
|
||||
} catch (\Throwable $e) { /* ignore */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ switch ($role) {
|
||||
} catch (\Throwable $e) {
|
||||
$year = $sess->get('school_year') ?? $configModel->getConfig('school_year');
|
||||
}
|
||||
$semester = $sess->get('semester') ?? $configModel->getConfig('semester');
|
||||
$semester = $sess->get('semester') ?? getSemester();
|
||||
$activeEventCount = count($eventModel->getActiveEvents($year, $semester) ?? []);
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
value="<?= esc($searchTermUsedInSearch ?? '') ?>">
|
||||
<button class="btn btn-outline-secondary" type="button" id="manualPayClearSearch">Clear</button>
|
||||
<button class="btn btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
<div id="manualPaySuggest" class="list-group manual-pay-suggest d-none" role="listbox"></div>
|
||||
@@ -1001,9 +1002,11 @@
|
||||
function initSearchSuggest() {
|
||||
const input = _el('manualPaySearchInput');
|
||||
const suggest = _el('manualPaySuggest');
|
||||
const clear = _el('manualPayClearSearch');
|
||||
if (!input || !suggest) return;
|
||||
|
||||
const suggestUrl = <?= json_encode(site_url('payment/manual_pay_suggest')) ?>;
|
||||
const manualPayUrl = <?= json_encode(site_url('payment/manual_pay')) ?>;
|
||||
let lastRequest = 0;
|
||||
|
||||
function hideSuggest() {
|
||||
@@ -1089,6 +1092,14 @@
|
||||
if (e.key === 'Escape') hideSuggest();
|
||||
});
|
||||
|
||||
if (clear) {
|
||||
clear.addEventListener('click', () => {
|
||||
input.value = '';
|
||||
hideSuggest();
|
||||
window.location.href = manualPayUrl;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target === input || suggest.contains(e.target)) return;
|
||||
hideSuggest();
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
<?= $this->extend('layout/main_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php $isSchoolYearReadonly = (bool) ($isSchoolYearReadonly ?? false); ?>
|
||||
<?php
|
||||
$isSchoolYearReadonly = (bool) ($isSchoolYearReadonly ?? false);
|
||||
$successMessage = session()->getFlashdata('success');
|
||||
$errorMessage = session()->getFlashdata('error');
|
||||
?>
|
||||
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Print/Copy Requests</h2>
|
||||
<?php if ($successMessage): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<?= esc($successMessage) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($errorMessage): ?>
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<?= esc($errorMessage) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($isSchoolYearReadonly): ?>
|
||||
<div class="alert alert-warning">
|
||||
This school year is read-only. Existing requests are visible, but changes are disabled.
|
||||
@@ -46,6 +62,7 @@
|
||||
<div class="tab-pane fade show active" id="tab-print" role="tabpanel" aria-labelledby="tab-print-tab">
|
||||
<form action="<?= site_url('print-requests/create') ?>" method="post" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="request_token" value="<?= esc($printRequestToken ?? '') ?>">
|
||||
<?php if (session()->has('errors')): ?>
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
@@ -128,6 +145,7 @@
|
||||
</div>
|
||||
<form id="copyRequestForm" method="post" action="<?= site_url('print-requests/create-copy') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="request_token" value="<?= esc($copyRequestToken ?? '') ?>">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="copy_num_copies" class="form-label">Number of Copies</label>
|
||||
@@ -353,8 +371,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const dateSelect = document.getElementById('required_by_date');
|
||||
const timeSelect = document.getElementById('required_by_time');
|
||||
const hiddenInput = document.getElementById('required_by');
|
||||
const submitButton = createForm.querySelector('button[type="submit"]');
|
||||
if (dateSelect.value && timeSelect.value) {
|
||||
hiddenInput.value = dateSelect.value + 'T' + timeSelect.value;
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.dataset.originalText = submitButton.textContent;
|
||||
submitButton.textContent = 'Submitting...';
|
||||
}
|
||||
} else {
|
||||
if (!dateSelect.value) {
|
||||
alert('Please select a date.');
|
||||
@@ -483,6 +507,7 @@ if (copyForm) {
|
||||
const dateSelect = document.getElementById('copy_required_by_date');
|
||||
const timeSelect = document.getElementById('copy_required_by_time');
|
||||
const hiddenInput = document.getElementById('copy_required_by');
|
||||
const submitButton = copyForm.querySelector('button[type="submit"]');
|
||||
if (!dateSelect || !dateSelect.value) {
|
||||
alert('Please select a date for the copy request.');
|
||||
e.preventDefault();
|
||||
@@ -496,6 +521,11 @@ if (copyForm) {
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = dateSelect.value + 'T' + timeSelect.value;
|
||||
}
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.dataset.originalText = submitButton.textContent;
|
||||
submitButton.textContent = 'Submitting...';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,384 +1,3 @@
|
||||
|
||||
Yesterday 11:12 PM
|
||||
write sql querry to update school_year = 2025-2026
|
||||
|
||||
UPDATE your_table_name
|
||||
SET school_year = '2025-2026'
|
||||
WHERE your_condition;
|
||||
To update every row:
|
||||
|
||||
UPDATE your_table_name
|
||||
SET school_year = '2025-2026';
|
||||
Use the second version only if you genuinely intend to rewrite the entire table, because databases are remarkably obedient when humans forget WHERE.
|
||||
|
||||
|
||||
Today 12:49 AM
|
||||
Screenshot 2026-07-14 at 12.49.07 AM.png
|
||||
|
||||
Pasted code(6).php
|
||||
File
|
||||
remove the image content from the view
|
||||
|
||||
Removed the School year / Semester filter from the view by deleting:
|
||||
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
Pasted_code_6_without_academic_filter.php
|
||||
|
||||
|
||||
Library
|
||||
/
|
||||
Pasted_code_6_without_academic_filter.php
|
||||
|
||||
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
// Role formatter for display + posting
|
||||
$formatRole = function (?string $role): string {
|
||||
$role = (string)$role;
|
||||
$role = str_replace(['-', '_'], ' ', $role);
|
||||
$role = preg_replace('/\s+/', ' ', trim($role));
|
||||
if ($role === '') return '';
|
||||
$out = [];
|
||||
foreach (explode(' ', $role) as $w) {
|
||||
if ($w === '') continue;
|
||||
if (preg_match('/^[A-Za-z]{1,3}$/', $w)) {
|
||||
$out[] = strtoupper($w); // TA, PTA, HR, KG...
|
||||
} else {
|
||||
$out[] = ucfirst(strtolower($w)); // Teacher, Assistant, Admin...
|
||||
}
|
||||
}
|
||||
return implode(' ', $out);
|
||||
};
|
||||
?>
|
||||
<div class="container-fluid">
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
|
||||
<div class="text-center mx-auto mb-5 wow" data-wow-delay="0.1s" style="max-width: 600px;">
|
||||
<br>
|
||||
<h2 class="text-dark mb-3" style="font-family: Arial, sans-serif;">Generate Staff Badges</h2>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-md-row justify-content-md-end align-items-md-center gap-2">
|
||||
<!-- Submit button associates to form below -->
|
||||
<button type="submit" class="btn btn-success mt-3" form="badgeForm">Generate Badges</button>
|
||||
<br>
|
||||
</div>
|
||||
<!-- Staff selection -->
|
||||
<form method="post" action="<?= base_url('badge') ?>" target="_blank" id="badgeForm">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<table id="staffTable" class="table table-bordered table-striped align-middle w-100">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 70px;">#</th>
|
||||
<th scope="col">Firstname</th>
|
||||
<th scope="col">Lastname</th>
|
||||
<th scope="col">Role</th>
|
||||
<th scope="col">Teacher Class Section</th>
|
||||
<th scope="col" style="width: 140px;">Badge Prints</th>
|
||||
<th scope="col" style="width: 150px;">
|
||||
<div class="form-check m-0">
|
||||
<input class="form-check-input" type="checkbox" id="select-all">
|
||||
<label class="form-check-label" for="select-all">Select All (page)</label>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($users)): ?>
|
||||
<?php $order = 1; ?>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<?php
|
||||
// Pick a representative role (first of CSV or role_name/active_role)
|
||||
$roleRaw = $user['active_role'] ?? $user['role_name'] ?? ($user['roles'] ?? '');
|
||||
if (strpos((string)$roleRaw, ',') !== false) {
|
||||
$parts = array_filter(array_map('trim', explode(',', (string)$roleRaw)));
|
||||
$roleRaw = $parts[0] ?? '';
|
||||
}
|
||||
//$roleLabel = $roleRaw !== '' ? $formatRole($roleRaw) : '-';
|
||||
$roleLabel = $user['role_name'] ?? ($user['roles'] ?? '-'); // both are pre-formatted by $formatRole
|
||||
|
||||
// Normalize user id key for checkbox
|
||||
$uid = $user['user_id'] ?? $user['id'] ?? ($user['users.id'] ?? null);
|
||||
|
||||
// Class section name (optional)
|
||||
$className = !empty($user['class_section_name']) ? (string)$user['class_section_name'] : '';
|
||||
?>
|
||||
<tr
|
||||
data-user-id="<?= esc($uid ?? '') ?>"
|
||||
data-role-label="<?= esc($roleLabel) ?>"
|
||||
data-class-name="<?= esc($className) ?>">
|
||||
<td style="text-align: center;"><?= esc($order++) ?></td>
|
||||
<td><?= esc($user['firstname'] ?? '') ?></td>
|
||||
<td><?= esc($user['lastname'] ?? '') ?></td>
|
||||
<td><?= esc($roleLabel) ?></td>
|
||||
<td><?= $className !== '' ? esc($className) : '-' ?></td>
|
||||
<td><span class="badge bg-secondary prints-badge" data-user-id="<?= esc($uid ?? '') ?>">—</span></td>
|
||||
<td>
|
||||
<?php if ($uid !== null): ?>
|
||||
<div class="form-check m-0">
|
||||
<input type="checkbox" name="user_ids[]" value="<?= esc($uid) ?>" class="form-check-input user-checkbox" id="chk-<?= esc($uid) ?>">
|
||||
<label class="form-check-label" for="chk-<?= esc($uid) ?>">Select</label>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">N/A</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted">No staff found for the selected year.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Track selected user IDs across paging/filtering/sorting
|
||||
const selected = new Set();
|
||||
const formEl = document.getElementById('badgeForm');
|
||||
let clearTimerId = null; // pending auto-clear timer, if any
|
||||
let hiddenContainer = document.getElementById('selectedHiddenInputs');
|
||||
// Ensure hidden container exists inside the form (some earlier layouts had it outside)
|
||||
if (!hiddenContainer || !formEl.contains(hiddenContainer)) {
|
||||
hiddenContainer = document.createElement('div');
|
||||
hiddenContainer.id = 'selectedHiddenInputs';
|
||||
hiddenContainer.style.display = 'none';
|
||||
formEl.appendChild(hiddenContainer);
|
||||
}
|
||||
const selectAll = document.getElementById('select-all');
|
||||
const csrfName = <?= json_encode(csrf_token()) ?>;
|
||||
const csrfInput = document.querySelector(`input[name='${csrfName}']`);
|
||||
|
||||
// Build a meta map (userId -> { role, className }) from the DOM BEFORE DataTables paginates
|
||||
const rowMeta = {};
|
||||
document.querySelectorAll('#staffTable tbody tr').forEach(tr => {
|
||||
const id = tr.getAttribute('data-user-id');
|
||||
if (!id) return;
|
||||
rowMeta[id] = {
|
||||
role: tr.getAttribute('data-role-label') || '',
|
||||
className: tr.getAttribute('data-class-name') || ''
|
||||
};
|
||||
});
|
||||
|
||||
const table = $('#staffTable').DataTable({
|
||||
stateSave: true,
|
||||
pageLength: 100,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
order: [
|
||||
[1, 'asc'],
|
||||
[2, 'asc']
|
||||
], // Firstname, Lastname
|
||||
columnDefs: [{
|
||||
targets: 0,
|
||||
searchable: false
|
||||
}, // row #
|
||||
{
|
||||
targets: 6,
|
||||
orderable: false,
|
||||
searchable: false
|
||||
} // checkbox column
|
||||
]
|
||||
});
|
||||
|
||||
// Helper: keep hidden inputs in sync so selections submit even if not on current page
|
||||
function syncHiddenInputs() {
|
||||
hiddenContainer.innerHTML = '';
|
||||
selected.forEach(function(id) {
|
||||
const meta = rowMeta[id] || {
|
||||
role: '',
|
||||
className: ''
|
||||
};
|
||||
// user_ids[]
|
||||
const i1 = document.createElement('input');
|
||||
i1.type = 'hidden';
|
||||
i1.name = 'user_ids[]';
|
||||
i1.value = id;
|
||||
hiddenContainer.appendChild(i1);
|
||||
// roles[ID]
|
||||
const i2 = document.createElement('input');
|
||||
i2.type = 'hidden';
|
||||
i2.name = `roles[${id}]`;
|
||||
i2.value = meta.role;
|
||||
hiddenContainer.appendChild(i2);
|
||||
// classes[ID]
|
||||
const i3 = document.createElement('input');
|
||||
i3.type = 'hidden';
|
||||
i3.name = `classes[${id}]`;
|
||||
i3.value = meta.className;
|
||||
hiddenContainer.appendChild(i3);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper: refresh checkboxes on current page based on Set
|
||||
function syncPageCheckboxes() {
|
||||
const pageNodes = table.rows({
|
||||
page: 'current'
|
||||
}).nodes().to$();
|
||||
let allChecked = true;
|
||||
let anyChecked = false;
|
||||
pageNodes.each(function() {
|
||||
const row = this;
|
||||
const userId = row.getAttribute('data-user-id');
|
||||
const cb = row.querySelector('.user-checkbox');
|
||||
if (!cb || !userId) return;
|
||||
cb.checked = selected.has(userId);
|
||||
if (!cb.checked) allChecked = false;
|
||||
if (cb.checked) anyChecked = true;
|
||||
});
|
||||
// Update header select-all for current page
|
||||
selectAll.checked = allChecked && pageNodes.length > 0;
|
||||
selectAll.indeterminate = !allChecked && anyChecked;
|
||||
}
|
||||
|
||||
// Helper: clear all selections and reset the current page UI
|
||||
function clearSelectionsNow() {
|
||||
selected.clear();
|
||||
// Uncheck visible checkboxes on current page
|
||||
table.rows({ page: 'current' }).every(function () {
|
||||
const row = this.node();
|
||||
const cb = row.querySelector('.user-checkbox');
|
||||
if (cb) cb.checked = false;
|
||||
});
|
||||
// Reset select-all and hidden inputs
|
||||
selectAll.checked = false;
|
||||
selectAll.indeterminate = false;
|
||||
syncHiddenInputs();
|
||||
syncPageCheckboxes();
|
||||
}
|
||||
|
||||
// On draw (paging/sort/search), just refresh checkbox UI states
|
||||
table.on('draw', function() {
|
||||
syncPageCheckboxes();
|
||||
});
|
||||
|
||||
// Delegate checkbox change handling once at the table level (works across redraws)
|
||||
document.getElementById('staffTable').addEventListener('change', function (e) {
|
||||
const target = e.target;
|
||||
if (!target || !target.classList.contains('user-checkbox')) return;
|
||||
const tr = target.closest('tr[data-user-id]');
|
||||
const userId = tr ? tr.getAttribute('data-user-id') : null;
|
||||
if (!userId) return;
|
||||
if (target.checked) selected.add(userId); else selected.delete(userId);
|
||||
syncHiddenInputs();
|
||||
syncPageCheckboxes();
|
||||
// If user interacts, cancel any pending auto-clear to avoid wiping new selections
|
||||
if (clearTimerId !== null) { clearTimeout(clearTimerId); clearTimerId = null; }
|
||||
});
|
||||
|
||||
// Initial sync on first load
|
||||
table.draw(false);
|
||||
|
||||
// Header "Select All (page)" toggler
|
||||
selectAll.addEventListener('change', function() {
|
||||
const check = this.checked;
|
||||
table.rows({
|
||||
page: 'current'
|
||||
}).every(function() {
|
||||
const row = this.node();
|
||||
const userId = row.getAttribute('data-user-id');
|
||||
const cb = row.querySelector('.user-checkbox');
|
||||
if (!cb || !userId) return;
|
||||
cb.checked = check;
|
||||
if (check) selected.add(userId);
|
||||
else selected.delete(userId);
|
||||
});
|
||||
syncHiddenInputs();
|
||||
syncPageCheckboxes();
|
||||
if (clearTimerId !== null) { clearTimeout(clearTimerId); clearTimerId = null; }
|
||||
});
|
||||
|
||||
// Before submit, ensure CSRF is fresh and hidden inputs include all selections
|
||||
const form = document.getElementById('badgeForm');
|
||||
async function refreshCsrf() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (!resp.ok) return;
|
||||
const json = await resp.json();
|
||||
if (json && json.csrf_token && json.csrf_hash && csrfInput && json.csrf_token === csrfName) {
|
||||
csrfInput.value = json.csrf_hash;
|
||||
}
|
||||
} catch (e) {
|
||||
// No-op: if refresh fails we'll still attempt submit; server may accept existing token
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
if (selected.size === 0) {
|
||||
alert('Please select at least one staff member to generate badges.');
|
||||
return;
|
||||
}
|
||||
syncHiddenInputs();
|
||||
// CSRF is excluded for this endpoint now, but keeping refresh is safe
|
||||
try { await refreshCsrf(); } catch (_) {}
|
||||
// Native submit so browser handles PDF in a new tab
|
||||
form.submit();
|
||||
// Auto-clear selections 5 seconds after generating badges
|
||||
clearTimerId = setTimeout(function() { clearSelectionsNow(); clearTimerId = null; }, 5000);
|
||||
});
|
||||
|
||||
// --- Fetch and render print status ---
|
||||
const selectedYear = <?= json_encode($selectedYear ?? '') ?>;
|
||||
|
||||
function fetchPrintStatus() {
|
||||
const ids = Object.keys(rowMeta);
|
||||
if (ids.length === 0) return;
|
||||
const params = new URLSearchParams();
|
||||
ids.forEach(id => params.append('user_ids[]', id));
|
||||
if (selectedYear) params.append('school_year', selectedYear);
|
||||
|
||||
fetch('<?= base_url('api/printables/badges/status') ?>?' + params.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(r => r.ok ? r.json() : Promise.reject())
|
||||
.then(json => {
|
||||
if (!json || !json.ok || !json.data) return;
|
||||
const data = json.data;
|
||||
Object.keys(rowMeta).forEach(id => {
|
||||
const info = data[id];
|
||||
const badge = document.querySelector(`.prints-badge[data-user-id="${id}"]`);
|
||||
const tr = document.querySelector(`#staffTable tbody tr[data-user-id="${id}"]`);
|
||||
if (!badge || !tr) return;
|
||||
const count = info ? (info.count || 0) : 0;
|
||||
badge.textContent = count > 0 ? `Printed ${count}` : '—';
|
||||
badge.className = 'badge prints-badge ' + (count > 0 ? 'bg-success' : 'bg-secondary');
|
||||
tr.classList.toggle('table-success', count > 0);
|
||||
});
|
||||
// Refresh CSRF for subsequent submissions (so no page reload is needed)
|
||||
if (json.csrf_token && json.csrf_hash && csrfInput && json.csrf_token === csrfName) {
|
||||
csrfInput.value = json.csrf_hash;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
fetchPrintStatus();
|
||||
window.addEventListener('focus', fetchPrintStatus);
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
Library
|
||||
/
|
||||
Pasted_code_6_without_academic_filter.php
|
||||
|
||||
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?php
|
||||
|
||||
+79
-80
@@ -34,112 +34,104 @@
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="d-flex justify-content-end mb-2 gap-2 flex-wrap">
|
||||
<form method="post" action="/refunds/recalculateOverpayments" class="d-flex gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-outline-primary btn-sm" type="submit" title="Current school year">
|
||||
Recalculate Overpayments (This Year)
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/refunds/recalculateOverpayments" class="d-flex gap-2">
|
||||
<?= csrf_field() ?>
|
||||
<input type="text" name="invoice_number" class="form-control form-control-sm" placeholder="Invoice # (e.g., INV-...)" style="min-width: 260px;">
|
||||
<button class="btn btn-outline-secondary btn-sm" type="submit" title="Recalc a specific invoice across any year">
|
||||
Recalc Specific Invoice
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="refundsTable" class="table table-bordered table-striped align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>School ID</th>
|
||||
<th>Parent</th>
|
||||
<th>Request</th>
|
||||
<th>Term</th>
|
||||
<th>Invoice ID</th>
|
||||
<th class="text-end">Refund Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Invoice #</th>
|
||||
<th>Requested</th>
|
||||
<th>Approved</th>
|
||||
<th>Approved By</th>
|
||||
<th>Refunded</th>
|
||||
<th>Method</th>
|
||||
<th>Check #</th>
|
||||
<th>Check File</th>
|
||||
<th class="text-end">Paid Amount</th>
|
||||
<th class="text-end">Source Available</th>
|
||||
<th class="text-end">Parent Available</th>
|
||||
<th>Status</th>
|
||||
<th>Refund Details</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($refunds as $r): ?>
|
||||
<?php
|
||||
// Friendly badges
|
||||
$req = strtolower((string)($r['request'] ?? ''));
|
||||
$reqBadgeClass = [
|
||||
'tuition' => 'primary',
|
||||
'overpayment' => 'success',
|
||||
'extra' => 'info',
|
||||
'duplicate' => 'warning',
|
||||
][$req] ?? 'secondary';
|
||||
|
||||
// Date formatting (keep raw if null)
|
||||
$fmt = function($dt) {
|
||||
if (empty($dt) || $dt === '0000-00-00 00:00:00') return '-';
|
||||
// show date only; assumes DB is UTC
|
||||
return local_date($dt, 'm-d-Y');
|
||||
};
|
||||
$statusRaw = (string)($r['status'] ?? '');
|
||||
$statusKey = strtolower(str_replace(' ', '_', trim($statusRaw)));
|
||||
if ($statusKey === 'requested') {
|
||||
$statusKey = 'pending';
|
||||
} elseif ($statusKey === 'partially_paid') {
|
||||
$statusKey = 'partial';
|
||||
}
|
||||
$statusLabel = [
|
||||
'pending' => 'Pending',
|
||||
'approved' => 'Approved',
|
||||
'rejected' => 'Rejected',
|
||||
'partial' => 'Partial',
|
||||
'paid' => 'Paid',
|
||||
][$statusKey] ?? ($statusRaw !== '' ? $statusRaw : 'Pending');
|
||||
$statusClass = [
|
||||
'pending' => 'warning text-dark',
|
||||
'approved' => 'primary',
|
||||
'rejected' => 'danger',
|
||||
'partial' => 'info text-dark',
|
||||
'paid' => 'success',
|
||||
][$statusKey] ?? 'secondary';
|
||||
$refundAmount = (float)($r['refund_amount'] ?? 0);
|
||||
$paidAmount = (float)($r['refund_paid_amount'] ?? 0);
|
||||
$remainingAmount = max(0, $refundAmount - $paidAmount);
|
||||
$hasSource = !empty($r['source_type']) && !empty($r['source_id']);
|
||||
$canApprove = $statusKey === 'pending' && $refundAmount > 0 && $hasSource;
|
||||
$canReject = $statusKey === 'pending';
|
||||
$canRecord = in_array($statusKey, ['approved', 'partial'], true) && $remainingAmount > 0;
|
||||
?>
|
||||
<tr>
|
||||
<td><?= esc($r['school_id']) ?></td>
|
||||
<td><?= esc(($r['firstname'] ?? '').' '.($r['lastname'] ?? '')) ?></td>
|
||||
<td><?= esc($r['invoice_number'] ?? '-') ?></td>
|
||||
<td>
|
||||
<?php if ($req): ?>
|
||||
<span class="badge bg-<?= $reqBadgeClass ?> text-uppercase"><?= esc($req) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">-</span>
|
||||
<div class="fw-semibold">$<?= esc(number_format($refundAmount, 2)) ?></div>
|
||||
<div class="text-muted small"><?= esc($fmt($r['requested_at'] ?? null)) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><span class="badge bg-<?= esc($statusClass) ?>"><?= esc($statusLabel) ?></span></div>
|
||||
<?php if (!empty($r['approved_at']) && $r['approved_at'] !== '0000-00-00 00:00:00'): ?>
|
||||
<div class="text-muted small mt-1">
|
||||
<?= esc($fmt($r['approved_at'])) ?>
|
||||
<?php if (!empty($r['approved_by_name']) && $r['approved_by_name'] !== '-'): ?>
|
||||
by <?= esc($r['approved_by_name']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= esc(($r['school_year'] ?? '-'). ' / ' . ($r['semester'] ?? '-')) ?></td>
|
||||
<td><?= esc($r['invoice_id'] ?? '-') ?></td>
|
||||
<td class="text-end">$<?= esc(number_format((float)$r['refund_amount'], 2)) ?></td>
|
||||
<td><?= esc($r['status']) ?></td>
|
||||
<td><?= esc($fmt($r['requested_at'] ?? null)) ?></td>
|
||||
<td><?= esc($fmt($r['approved_at'] ?? null)) ?></td>
|
||||
<td><?= esc($r['approved_by_name'] ?? '-') ?></td>
|
||||
<td><?= esc($fmt($r['refunded_at'] ?? null)) ?></td>
|
||||
<td><?= esc($r['refund_method'] ?? '-') ?></td>
|
||||
<td><?= esc($r['check_nbr'] ?? '-') ?></td>
|
||||
<td>
|
||||
<?php if (!empty($r['check_file'])): ?>
|
||||
<a href="<?= base_url('refunds/file/' . (int) $r['id'] . '/inline') ?>" target="_blank">View</a>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
<div><span class="text-muted small">Check #:</span> <?= esc($r['check_nbr'] ?? '-') ?></div>
|
||||
<div>
|
||||
<span class="text-muted small">Check File:</span>
|
||||
<?php if (!empty($r['check_file'])): ?>
|
||||
<a href="<?= base_url('refunds/file/' . (int) $r['id'] . '/inline') ?>" target="_blank">View</a>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div><span class="text-muted small">Paid:</span> $<?= esc(number_format($paidAmount, 2)) ?></div>
|
||||
</td>
|
||||
<td class="text-end">$<?= esc(number_format((float)$r['refund_paid_amount'], 2)) ?></td>
|
||||
<td class="text-end">
|
||||
<?= $r['available_refundable_credit'] === null ? '-' : '$' . esc(number_format((float)$r['available_refundable_credit'], 2)) ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<?= isset($r['parent_available_refundable_credit']) ? '$' . esc(number_format((float)$r['parent_available_refundable_credit'], 2)) : '-' ?>
|
||||
</td>
|
||||
<td class="d-flex gap-2">
|
||||
<td>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-success btn-sm"
|
||||
onclick="handleRecordRefundClick(<?= (int)$r['id'] ?>, '<?= esc($r['status']) ?>', <?= (float)$r['refund_amount'] ?>)">
|
||||
<?= $canRecord ? '' : 'disabled' ?>
|
||||
onclick="handleRecordRefundClick(<?= (int)$r['id'] ?>, '<?= esc($statusLabel) ?>', <?= $remainingAmount ?>)">
|
||||
Record
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm"
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Approved', <?= (float)$r['refund_amount'] ?>)">
|
||||
<?= $canApprove ? '' : 'disabled' ?>
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Approved', <?= $refundAmount ?>)">
|
||||
Approve
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm"
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Rejected', <?= (float)$r['refund_amount'] ?>)">
|
||||
<?= $canReject ? '' : 'disabled' ?>
|
||||
onclick="handleStatusClick(<?= (int)$r['id'] ?>, 'Rejected', <?= max($refundAmount, 0.01) ?>)">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -231,7 +223,7 @@ $(function () {
|
||||
// DataTable
|
||||
$('#refundsTable').DataTable({
|
||||
pageLength: 25,
|
||||
order: [[7, 'desc']], // order by Requested desc
|
||||
order: [[2, 'desc']], // order by Requested desc
|
||||
scrollX: true,
|
||||
autoWidth: false,
|
||||
});
|
||||
@@ -277,21 +269,27 @@ $(function () {
|
||||
});
|
||||
|
||||
// ---- Actions ----
|
||||
function normalizeRefundStatus(status) {
|
||||
return String(status || '').trim().toLowerCase().replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
function handleRecordRefundClick(refundId, status, amount) {
|
||||
if (!['Approved','Partial'].includes(status)) {
|
||||
alert('⚠ Refund must be Approved (or Partial) before recording a payout.');
|
||||
const normalized = normalizeRefundStatus(status);
|
||||
if (!['approved','partial','partially_paid'].includes(normalized)) {
|
||||
alert('Refund must be approved or partial before recording a payout.');
|
||||
return;
|
||||
}
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
alert('⚠ Refund amount not set.');
|
||||
alert('Refund amount is not set.');
|
||||
return;
|
||||
}
|
||||
showPaymentModal(refundId);
|
||||
showPaymentModal(refundId, amount);
|
||||
}
|
||||
|
||||
function handleStatusClick(refundId, status, amount) {
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
alert('⚠ Refund amount not set.');
|
||||
const normalized = normalizeRefundStatus(status);
|
||||
if (normalized !== 'rejected' && (!amount || parseFloat(amount) <= 0)) {
|
||||
alert('Refund amount is not set.');
|
||||
return;
|
||||
}
|
||||
showStatusModal(refundId, status);
|
||||
@@ -308,7 +306,7 @@ function showStatusModal(refundId, status) {
|
||||
$('#statusModal').modal('show');
|
||||
}
|
||||
|
||||
function showPaymentModal(refundId) {
|
||||
function showPaymentModal(refundId, amount) {
|
||||
$('#statusForm').addClass('d-none');
|
||||
$('#paymentForm').removeClass('d-none');
|
||||
|
||||
@@ -318,7 +316,8 @@ function showPaymentModal(refundId) {
|
||||
? crypto.randomUUID()
|
||||
: ('refund-' + refundId + '-' + Date.now() + '-' + Math.random().toString(16).slice(2))
|
||||
);
|
||||
$('#paidAmount').val('');
|
||||
$('#paidAmount').val(Number(amount || 0).toFixed(2));
|
||||
$('#paidAmount').attr('max', Number(amount || 0).toFixed(2));
|
||||
$('#paymentMethod').val('').trigger('change');
|
||||
$('#checkDetails').addClass('d-none');
|
||||
$('#checkNumber').val('');
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
} catch (\Throwable $e) {
|
||||
$sy = $configModel->getConfig('school_year');
|
||||
}
|
||||
$sem = $configModel->getConfig('semester');
|
||||
$sem = getSemester();
|
||||
$classOptions = $teacherClassModel->getClassAssignmentsByUserId((int)$userId, (string)$sy, (string)$sem);
|
||||
}
|
||||
?>
|
||||
|
||||
Reference in New Issue
Block a user