488 lines
20 KiB
PHP
Executable File
488 lines
20 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Controllers\View;
|
|
|
|
use App\Controllers\BaseController;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
use App\Models\InvoiceModel;
|
|
use App\Models\PaymentModel;
|
|
use App\Models\StudentClassModel;
|
|
use App\Models\ConfigurationModel;
|
|
|
|
class FamilyAdminController extends BaseController
|
|
{
|
|
public function index(): ResponseInterface
|
|
{
|
|
$db = \Config\Database::connect();
|
|
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
|
|
$guardianId = (int) ($this->request->getGet('guardian_id') ?? 0);
|
|
$data = ['student' => null, 'families' => [], 'guardians' => [], 'students' => []];
|
|
|
|
// Simple student list for select
|
|
$data['students'] = $db->query("SELECT id, CONCAT(lastname, ', ', firstname) AS name FROM students ORDER BY lastname, firstname")->getResultArray();
|
|
// Preload search datasets (students + guardians/parents)
|
|
$data['searchStudents'] = $db->query("SELECT id, firstname, lastname FROM students ORDER BY lastname, firstname")->getResultArray();
|
|
$data['searchGuardians'] = $db->query(
|
|
"SELECT DISTINCT u.id, u.firstname, u.lastname, u.email, u.cellphone
|
|
FROM family_guardians fg
|
|
JOIN users u ON u.id = fg.user_id
|
|
ORDER BY u.lastname, u.firstname"
|
|
)->getResultArray();
|
|
|
|
// If a guardian is provided, resolve one of their students and redirect to use existing flow
|
|
if (!$studentId && $guardianId) {
|
|
$row = $db->query(
|
|
"SELECT s.id AS student_id
|
|
FROM family_guardians fg
|
|
JOIN family_students fs ON fs.family_id = fg.family_id
|
|
JOIN students s ON s.id = fs.student_id
|
|
WHERE fg.user_id = ?
|
|
ORDER BY s.lastname, s.firstname
|
|
LIMIT 1",
|
|
[$guardianId]
|
|
)->getRowArray();
|
|
if (!$row) {
|
|
$row = $db->query(
|
|
"SELECT s.id AS student_id
|
|
FROM students s
|
|
WHERE s.parent_id = ?
|
|
ORDER BY s.lastname, s.firstname
|
|
LIMIT 1",
|
|
[$guardianId]
|
|
)->getRowArray();
|
|
}
|
|
if ($row && !empty($row['student_id'])) {
|
|
return redirect()->to(site_url('family?student_id='.(int)$row['student_id']));
|
|
}
|
|
}
|
|
|
|
if ($studentId) {
|
|
$data['student'] = $db->query("SELECT id, firstname, lastname FROM students WHERE id = ?", [$studentId])->getRowArray();
|
|
|
|
// Fetch all families for this student with extended fields
|
|
$families = $db->query(
|
|
"SELECT f.id, f.family_code, f.household_name, f.address_line1, f.address_line2, f.city, f.state, f.postal_code, f.country,
|
|
f.primary_phone, f.preferred_lang, f.preferred_contact_method, f.is_active,
|
|
fs.is_primary_home
|
|
FROM family_students fs
|
|
JOIN families f ON f.id = fs.family_id
|
|
WHERE fs.student_id = ?
|
|
ORDER BY fs.is_primary_home DESC, f.household_name",
|
|
[$studentId]
|
|
)->getResultArray();
|
|
|
|
// Enrich each family with guardians, students, and financials
|
|
$invoiceModel = new InvoiceModel();
|
|
$paymentModel = new PaymentModel();
|
|
$studentClassModel = new StudentClassModel();
|
|
$configModel = new ConfigurationModel();
|
|
$schoolYear = (string) ($configModel->getConfig('school_year') ?? '');
|
|
|
|
foreach ($families as &$fam) {
|
|
$fid = (int) $fam['id'];
|
|
|
|
// Guardians (with contact info)
|
|
$guardians = $db->query(
|
|
"SELECT u.id AS user_id, u.firstname, u.lastname, u.email, u.cellphone,
|
|
u.address_street, u.city, u.state, u.zip,
|
|
fg.relation, fg.is_primary, fg.receive_emails, fg.receive_sms
|
|
FROM family_guardians fg
|
|
JOIN users u ON u.id = fg.user_id
|
|
WHERE fg.family_id = ?
|
|
ORDER BY fg.is_primary DESC, u.lastname, u.firstname",
|
|
[$fid]
|
|
)->getResultArray();
|
|
$fam['guardians'] = $guardians;
|
|
|
|
// Students in this family
|
|
$studentsRows = $db->query(
|
|
"SELECT s.id, s.firstname, s.lastname
|
|
FROM family_students fs
|
|
JOIN students s ON s.id = fs.student_id
|
|
WHERE fs.family_id = ?
|
|
ORDER BY s.lastname, s.firstname",
|
|
[$fid]
|
|
)->getResultArray();
|
|
// Enrich with grade label if available
|
|
if (!empty($studentsRows)) {
|
|
foreach ($studentsRows as &$sr) {
|
|
$sid = (int) ($sr['id'] ?? 0);
|
|
$sr['grade'] = $sid ? (string) ($studentClassModel->getClassSectionsByStudentId($sid, $schoolYear) ?? '') : '';
|
|
}
|
|
unset($sr);
|
|
}
|
|
$fam['students'] = $studentsRows;
|
|
|
|
// Financials: invoices and payments for all guardians (by user_id)
|
|
$parentIds = array_map(static fn($g) => (int)($g['user_id'] ?? 0), $guardians);
|
|
$parentIds = array_values(array_filter($parentIds));
|
|
|
|
$fam['invoices'] = [];
|
|
$fam['payments'] = [];
|
|
$fam['finance_summary'] = [
|
|
'invoices_count' => 0,
|
|
'total_amount' => 0.0,
|
|
'paid_amount' => 0.0,
|
|
'balance' => 0.0,
|
|
];
|
|
|
|
if (!empty($parentIds)) {
|
|
// Invoices
|
|
$invRows = $db->table('invoices')
|
|
->select('id, parent_id, invoice_number, status, total_amount, paid_amount, balance, issue_date, due_date')
|
|
->whereIn('parent_id', $parentIds)
|
|
->orderBy('issue_date', 'DESC')
|
|
->get()->getResultArray();
|
|
$fam['invoices'] = $invRows;
|
|
// Map invoice id -> number for payments table
|
|
$invoiceMap = [];
|
|
foreach ($invRows as $ir) {
|
|
$invoiceMap[(int)$ir['id']] = (string)($ir['invoice_number'] ?? '');
|
|
}
|
|
$fam['invoice_map'] = $invoiceMap;
|
|
|
|
// Aggregate summary
|
|
foreach ($invRows as $ir) {
|
|
$fam['finance_summary']['invoices_count']++;
|
|
$fam['finance_summary']['total_amount'] += (float)($ir['total_amount'] ?? 0);
|
|
$fam['finance_summary']['paid_amount'] += (float)($ir['paid_amount'] ?? 0);
|
|
$fam['finance_summary']['balance'] += (float)($ir['balance'] ?? 0);
|
|
}
|
|
|
|
// Recent payments (limit 10)
|
|
$payRows = $db->table('payments')
|
|
->select('id, parent_id, invoice_id, paid_amount, balance, payment_method, payment_date, status')
|
|
->whereIn('parent_id', $parentIds)
|
|
->orderBy('payment_date', 'DESC')
|
|
->limit(10)
|
|
->get()->getResultArray();
|
|
$fam['payments'] = $payRows;
|
|
}
|
|
}
|
|
unset($fam);
|
|
|
|
$data['families'] = $families;
|
|
|
|
// Back-compat: also expose guardians of first family for old UI pieces
|
|
if (!empty($families)) {
|
|
$familyId = (int)$families[0]['id'];
|
|
$data['guardians'] = $db->query(
|
|
"SELECT u.id, u.firstname, u.lastname, u.email, fg.relation, fg.is_primary, fg.receive_emails
|
|
FROM family_guardians fg JOIN users u ON u.id = fg.user_id
|
|
WHERE fg.family_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname",
|
|
[$familyId]
|
|
)->getResultArray();
|
|
}
|
|
}
|
|
|
|
return service('response')->setBody(view('family/index', $data));
|
|
}
|
|
|
|
// GET /family/search?q=..
|
|
public function search(): ResponseInterface
|
|
{
|
|
$db = \Config\Database::connect();
|
|
$q = trim((string)($this->request->getGet('q') ?? ''));
|
|
if ($q === '') {
|
|
return $this->response->setJSON(['items' => []]);
|
|
}
|
|
$qs = '%' . $db->escapeLikeString($q) . '%';
|
|
|
|
// Students suggestions
|
|
$students = $db->query(
|
|
"SELECT id, firstname, lastname
|
|
FROM students
|
|
WHERE CONCAT_WS(' ', firstname, lastname) LIKE ?
|
|
ORDER BY lastname, firstname
|
|
LIMIT 8",
|
|
[$qs]
|
|
)->getResultArray();
|
|
|
|
// Parents/Guardians suggestions (users)
|
|
$guardians = $db->query(
|
|
"SELECT id, firstname, lastname, email, cellphone
|
|
FROM users
|
|
WHERE (
|
|
CONCAT_WS(' ', firstname, lastname) LIKE ?
|
|
OR email LIKE ?
|
|
OR cellphone LIKE ?
|
|
)
|
|
ORDER BY lastname, firstname
|
|
LIMIT 8",
|
|
[$qs, $qs, $qs]
|
|
)->getResultArray();
|
|
|
|
$items = [];
|
|
foreach ($students as $s) {
|
|
$items[] = [
|
|
'type' => 'student',
|
|
'id' => (int)$s['id'],
|
|
'label'=> trim(($s['firstname'] ?? '').' '.($s['lastname'] ?? '')),
|
|
'sub' => 'Student',
|
|
];
|
|
}
|
|
foreach ($guardians as $g) {
|
|
$items[] = [
|
|
'type' => 'guardian',
|
|
'id' => (int)$g['id'],
|
|
'label' => trim(($g['firstname'] ?? '').' '.($g['lastname'] ?? '')),
|
|
'sub' => trim(($g['email'] ?? '').' '.($g['cellphone'] ?? '')),
|
|
];
|
|
}
|
|
|
|
return $this->response->setJSON(['items' => $items]);
|
|
}
|
|
|
|
// GET /family/card?student_id=.. | guardian_id=.. | family_id=..
|
|
public function card(): ResponseInterface
|
|
{
|
|
$db = \Config\Database::connect();
|
|
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
|
|
$guardianId = (int) ($this->request->getGet('guardian_id') ?? 0);
|
|
$familyId = (int) ($this->request->getGet('family_id') ?? 0);
|
|
|
|
if (!$familyId) {
|
|
if ($studentId) {
|
|
$row = $db->query(
|
|
"SELECT f.id
|
|
FROM family_students fs
|
|
JOIN families f ON f.id = fs.family_id
|
|
WHERE fs.student_id = ?
|
|
ORDER BY fs.is_primary_home DESC, f.household_name
|
|
LIMIT 1",
|
|
[$studentId]
|
|
)->getRowArray();
|
|
if (!empty($row['id'])) $familyId = (int) $row['id'];
|
|
} elseif ($guardianId) {
|
|
// 1) Try via guardians link
|
|
$row = $db->query(
|
|
"SELECT f.id
|
|
FROM family_guardians fg
|
|
JOIN families f ON f.id = fg.family_id
|
|
WHERE fg.user_id = ?
|
|
ORDER BY f.household_name
|
|
LIMIT 1",
|
|
[$guardianId]
|
|
)->getRowArray();
|
|
if (!empty($row['id'])) {
|
|
$familyId = (int) $row['id'];
|
|
} else {
|
|
// 2) Fallback via students.parent_id → family_students
|
|
$row = $db->query(
|
|
"SELECT f.id
|
|
FROM students s
|
|
JOIN family_students fs ON fs.student_id = s.id
|
|
JOIN families f ON f.id = fs.family_id
|
|
WHERE s.parent_id = ?
|
|
ORDER BY fs.is_primary_home DESC, f.household_name
|
|
LIMIT 1",
|
|
[$guardianId]
|
|
)->getRowArray();
|
|
if (!empty($row['id'])) $familyId = (int) $row['id'];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$familyId) {
|
|
return $this->response->setStatusCode(404)->setBody('<div class="p-3 text-danger">Family not found.</div>');
|
|
}
|
|
|
|
$family = $db->query(
|
|
"SELECT f.id, f.family_code, f.household_name, f.address_line1, f.address_line2, f.city, f.state, f.postal_code, f.country,
|
|
f.primary_phone, f.preferred_lang, f.preferred_contact_method, f.is_active
|
|
FROM families f
|
|
WHERE f.id = ?",
|
|
[$familyId]
|
|
)->getRowArray();
|
|
|
|
if (!$family) {
|
|
return $this->response->setStatusCode(404)->setBody('<div class="p-3 text-danger">Family not found.</div>');
|
|
}
|
|
|
|
// Hydrate with guardians, students (+grades), invoices, payments
|
|
$invoiceModel = new \App\Models\InvoiceModel();
|
|
$paymentModel = new \App\Models\PaymentModel();
|
|
$studentClassModel = new \App\Models\StudentClassModel();
|
|
$configModel = new \App\Models\ConfigurationModel();
|
|
$schoolYear = (string) ($configModel->getConfig('school_year') ?? '');
|
|
|
|
// Guardians
|
|
$guardians = $db->query(
|
|
"SELECT u.id AS user_id, u.firstname, u.lastname, u.email, u.cellphone,
|
|
u.address_street, u.city, u.state, u.zip,
|
|
fg.relation, fg.is_primary, fg.receive_emails, fg.receive_sms
|
|
FROM family_guardians fg
|
|
JOIN users u ON u.id = fg.user_id
|
|
WHERE fg.family_id = ?
|
|
ORDER BY fg.is_primary DESC, u.lastname, u.firstname",
|
|
[$familyId]
|
|
)->getResultArray();
|
|
$family['guardians'] = $guardians;
|
|
|
|
// Students
|
|
$studentsRows = $db->query(
|
|
"SELECT s.id, s.firstname, s.lastname
|
|
FROM family_students fs
|
|
JOIN students s ON s.id = fs.student_id
|
|
WHERE fs.family_id = ?
|
|
ORDER BY s.lastname, s.firstname",
|
|
[$familyId]
|
|
)->getResultArray();
|
|
if (!empty($studentsRows)) {
|
|
foreach ($studentsRows as &$sr) {
|
|
$sid = (int) ($sr['id'] ?? 0);
|
|
$sr['grade'] = $sid ? (string) ($studentClassModel->getClassSectionsByStudentId($sid, $schoolYear) ?? '') : '';
|
|
}
|
|
unset($sr);
|
|
}
|
|
$family['students'] = $studentsRows;
|
|
|
|
// Financials
|
|
$parentIds = array_map(static fn($g) => (int)($g['user_id'] ?? 0), $guardians);
|
|
$parentIds = array_values(array_filter($parentIds));
|
|
|
|
$family['invoices'] = [];
|
|
$family['payments'] = [];
|
|
$family['finance_summary'] = [
|
|
'invoices_count' => 0,
|
|
'total_amount' => 0.0,
|
|
'paid_amount' => 0.0,
|
|
'balance' => 0.0,
|
|
];
|
|
// Emergency contacts (by guardian/parent)
|
|
$family['emergency_contacts'] = [];
|
|
if (!empty($parentIds)) {
|
|
// Map guardian name by user_id
|
|
$gmap = [];
|
|
foreach ($guardians as $g) {
|
|
$gid = (int)($g['user_id'] ?? 0);
|
|
if ($gid > 0) $gmap[$gid] = trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? ''));
|
|
}
|
|
$ecRows = $db->table('emergency_contacts')
|
|
->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester, created_at, updated_at')
|
|
->whereIn('parent_id', $parentIds)
|
|
->orderBy('updated_at', 'DESC')
|
|
->get()->getResultArray();
|
|
if (!empty($ecRows)) {
|
|
foreach ($ecRows as &$ec) {
|
|
$pid = (int)($ec['parent_id'] ?? 0);
|
|
$ec['parent_label'] = $gmap[$pid] ?? ('Parent #'.$pid);
|
|
}
|
|
unset($ec);
|
|
$family['emergency_contacts'] = $ecRows;
|
|
}
|
|
}
|
|
|
|
if (!empty($parentIds)) {
|
|
// Invoices
|
|
$invRows = $db->table('invoices')
|
|
->select('id, parent_id, invoice_number, status, total_amount, paid_amount, balance, issue_date, due_date')
|
|
->whereIn('parent_id', $parentIds)
|
|
->orderBy('issue_date', 'DESC')
|
|
->get()->getResultArray();
|
|
$family['invoices'] = $invRows;
|
|
$invoiceMap = [];
|
|
foreach ($invRows as $ir) {
|
|
$invoiceMap[(int)$ir['id']] = (string)($ir['invoice_number'] ?? '');
|
|
}
|
|
$family['invoice_map'] = $invoiceMap;
|
|
|
|
foreach ($invRows as $ir) {
|
|
$family['finance_summary']['invoices_count']++;
|
|
$family['finance_summary']['total_amount'] += (float)($ir['total_amount'] ?? 0);
|
|
$family['finance_summary']['paid_amount'] += (float)($ir['paid_amount'] ?? 0);
|
|
$family['finance_summary']['balance'] += (float)($ir['balance'] ?? 0);
|
|
}
|
|
|
|
// Payments
|
|
$payRows = $db->table('payments')
|
|
->select('id, parent_id, invoice_id, paid_amount, balance, payment_method, payment_date, status')
|
|
->whereIn('parent_id', $parentIds)
|
|
->orderBy('payment_date', 'DESC')
|
|
->limit(10)
|
|
->get()->getResultArray();
|
|
$family['payments'] = $payRows;
|
|
}
|
|
|
|
return service('response')->setBody(view('family/card', ['f' => $family]));
|
|
}
|
|
|
|
public function composeEmail()
|
|
{
|
|
$to = trim((string)$this->request->getGet('to'));
|
|
$name = trim((string)$this->request->getGet('name'));
|
|
$returnUrl = trim((string)$this->request->getGet('return_url'));
|
|
if ($returnUrl === '') {
|
|
$returnUrl = trim((string)$this->request->getServer('HTTP_REFERER'));
|
|
}
|
|
if ($returnUrl === '') {
|
|
$returnUrl = site_url('family');
|
|
}
|
|
|
|
return view('family/compose_email', [
|
|
'to' => $to,
|
|
'name' => $name,
|
|
'return_url' => $returnUrl,
|
|
]);
|
|
}
|
|
|
|
public function sendComposeEmail()
|
|
{
|
|
if (!$this->request->is('post')) {
|
|
return redirect()->to(site_url('family'));
|
|
}
|
|
|
|
$to = trim((string)$this->request->getPost('to'));
|
|
$subject = trim((string)$this->request->getPost('subject'));
|
|
$html = (string)($this->request->getPost('html') ?? '');
|
|
$returnUrl = trim((string)$this->request->getPost('return_url'));
|
|
|
|
if ($returnUrl === '' || !$this->isLocalReturnUrl($returnUrl)) {
|
|
$returnUrl = site_url('family');
|
|
}
|
|
|
|
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
|
return redirect()->back()->withInput()->with('error', 'Please enter a valid email address.');
|
|
}
|
|
if ($subject === '') {
|
|
return redirect()->back()->withInput()->with('error', 'Subject is required.');
|
|
}
|
|
if (trim($html) === '') {
|
|
return redirect()->back()->withInput()->with('error', 'Email body is required.');
|
|
}
|
|
|
|
$wrapped = view('emails/custom_html', [
|
|
'subject' => $subject,
|
|
'body_html' => $html,
|
|
]);
|
|
|
|
$mailer = new \App\Controllers\View\EmailController();
|
|
$ok = $mailer->sendEmail($to, $subject, $wrapped, 'communication');
|
|
|
|
if ($ok) {
|
|
return redirect()->to($returnUrl)->with('status', 'Email sent.');
|
|
}
|
|
|
|
return redirect()->to($returnUrl)->with('error', 'Unable to send email.');
|
|
}
|
|
|
|
private function isLocalReturnUrl(string $url): bool
|
|
{
|
|
$url = trim($url);
|
|
if ($url === '') return false;
|
|
|
|
$parts = parse_url($url);
|
|
if ($parts === false) return false;
|
|
|
|
if (!empty($parts['scheme']) || !empty($parts['host'])) {
|
|
$base = parse_url(site_url('/'));
|
|
if (!$base || empty($base['host'])) return false;
|
|
$hostMatch = strcasecmp((string)($parts['host'] ?? ''), (string)$base['host']) === 0;
|
|
return $hostMatch;
|
|
}
|
|
|
|
// Relative URL (e.g., /family?x=1 or family?x=1)
|
|
return true;
|
|
}
|
|
}
|