add inventory and supply logic
This commit is contained in:
@@ -1,487 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
// MODELS you should already have (or create tiny ones if not)
|
||||
use App\Models\FamilyModel;
|
||||
use App\Models\FamilyGuardianModel;
|
||||
use App\Models\FamilyStudentModel;
|
||||
|
||||
// Your app's models
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* FamilyController
|
||||
*
|
||||
* Handles creating/linking Family ←→ Guardians (users) ←→ Students,
|
||||
* including bootstrap from existing schema where students.parent_id is the
|
||||
* “first parent”, and a second parent may be present as a users row
|
||||
* or only as an email (stub user creation).
|
||||
*/
|
||||
class FamilyController extends BaseController
|
||||
{
|
||||
protected FamilyModel $families;
|
||||
protected FamilyGuardianModel $guardians;
|
||||
protected FamilyStudentModel $familyStudents;
|
||||
|
||||
protected StudentModel $students;
|
||||
protected UserModel $users;
|
||||
|
||||
protected BaseConnection $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->families = new FamilyModel();
|
||||
$this->guardians = new FamilyGuardianModel();
|
||||
$this->familyStudents = new FamilyStudentModel();
|
||||
|
||||
$this->students = new StudentModel();
|
||||
$this->users = new UserModel();
|
||||
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION A — APIs your UI uses
|
||||
* =======================================================*/
|
||||
|
||||
// GET /api/students/{id}/families
|
||||
public function familiesByStudent(int $studentId)
|
||||
{
|
||||
$rows = $this->db->query(
|
||||
"SELECT f.*, fs.is_primary_home
|
||||
FROM family_students fs
|
||||
JOIN families f ON f.id = fs.family_id
|
||||
WHERE fs.student_id = ? AND f.is_active = 1
|
||||
ORDER BY fs.is_primary_home DESC, f.household_name",
|
||||
[$studentId]
|
||||
)->getResultArray();
|
||||
|
||||
return $this->response->setJSON(['data' => $rows]);
|
||||
}
|
||||
|
||||
// GET /api/families/{id}/guardians
|
||||
public function guardiansByFamily(int $familyId)
|
||||
{
|
||||
$rows = $this->db->query(
|
||||
"SELECT u.id AS user_id, u.firstname, u.lastname, u.email,
|
||||
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();
|
||||
|
||||
return $this->response->setJSON(['data' => $rows]);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION B — Bootstrap & Linking helpers
|
||||
* =======================================================*/
|
||||
|
||||
// POST /families/bootstrap
|
||||
// Creates/ensures families for every student with parent_id,
|
||||
// links the student to that family, and (optionally) links a second parent.
|
||||
public function bootstrap()
|
||||
{
|
||||
// Optionally restrict this to admins
|
||||
// if (! $this->userCan('families.bootstrap')) return $this->deny();
|
||||
|
||||
// Guard: ensure required tables exist
|
||||
foreach (['families','family_students','family_guardians'] as $t) {
|
||||
if (! $this->db->tableExists($t)) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => "Missing required table '{$t}'. Run migrations: php spark migrate",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
// 1) Get all distinct primary parents from students.parent_id
|
||||
$primaryParents = $this->db->query(
|
||||
"SELECT DISTINCT parent_id FROM students WHERE parent_id IS NOT NULL"
|
||||
)->getResultArray();
|
||||
|
||||
$created = 0;
|
||||
$linkedStudents = 0;
|
||||
$linkedGuardians = 0;
|
||||
|
||||
foreach ($primaryParents as $row) {
|
||||
$primaryUserId = (int) $row['parent_id'];
|
||||
if ($primaryUserId <= 0) continue;
|
||||
|
||||
$familyId = $this->ensureFamilyForPrimaryParent($primaryUserId, $created);
|
||||
|
||||
// Link all students of this primary parent
|
||||
$students = $this->db->query(
|
||||
"SELECT id FROM students WHERE parent_id = ?",
|
||||
[$primaryUserId]
|
||||
)->getResultArray();
|
||||
|
||||
foreach ($students as $s) {
|
||||
$linkedStudents += $this->attachStudentToFamily((int)$s['id'], $familyId);
|
||||
}
|
||||
|
||||
// Ensure primary parent is guardian on that family
|
||||
$linkedGuardians += $this->attachGuardianUser($familyId, $primaryUserId, 'primary', true, 1, 0);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
$payload = [
|
||||
'status' => $this->db->transStatus() ? 'ok' : 'error',
|
||||
'families_created' => $created,
|
||||
'students_linked' => $linkedStudents,
|
||||
'guardians_linked' => $linkedGuardians,
|
||||
];
|
||||
|
||||
// If accessed via GET (browser), redirect back with flash
|
||||
if (strtolower($this->request->getMethod()) === 'get') {
|
||||
$msg = json_encode($payload);
|
||||
return redirect()->to('/family')->with('message', "Bootstrap result: {$msg}");
|
||||
}
|
||||
|
||||
return $this->response->setJSON($payload);
|
||||
}
|
||||
|
||||
// POST /families/attach-second-by-user
|
||||
// body: student_id, user_id, relation='secondary'
|
||||
public function attachSecondByUser()
|
||||
{
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
$relation = (string) ($this->request->getPost('relation') ?? 'secondary');
|
||||
|
||||
if (!$studentId || !$userId) return $this->bad('student_id and user_id required');
|
||||
|
||||
$familyId = $this->familyIdFromStudentPrimary($studentId);
|
||||
if (!$familyId) return $this->bad('No primary family found for this student');
|
||||
|
||||
$rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0);
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok', 'attached' => $rows, 'family_id' => $familyId]);
|
||||
}
|
||||
|
||||
// POST /families/attach-second-by-email
|
||||
// body: student_id, email, firstname, lastname, relation='secondary'
|
||||
// Creates a stub user if email not in users, then links.
|
||||
public function attachSecondByEmail()
|
||||
{
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
$email = trim((string)$this->request->getPost('email'));
|
||||
$first = trim((string)$this->request->getPost('firstname'));
|
||||
$last = trim((string)$this->request->getPost('lastname'));
|
||||
$relation = (string) ($this->request->getPost('relation') ?? 'secondary');
|
||||
|
||||
if (!$studentId || !$email) return $this->bad('student_id and email required');
|
||||
|
||||
$familyId = $this->familyIdFromStudentPrimary($studentId);
|
||||
if (!$familyId) return $this->bad('No primary family found for this student');
|
||||
|
||||
$user = $this->users->where('email', $email)->first();
|
||||
if (!$user) {
|
||||
// Create a minimal/stub user; adjust fields to your schema
|
||||
$this->users->insert([
|
||||
'firstname' => $first ?: '',
|
||||
'lastname' => $last ?: '',
|
||||
'email' => $email,
|
||||
'status' => 'Active',
|
||||
'user_type' => 'secondary', // align with secondary guardian role
|
||||
]);
|
||||
$userId = (int) $this->users->getInsertID();
|
||||
} else {
|
||||
$userId = (int) $user['id'];
|
||||
}
|
||||
|
||||
$rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0);
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok', 'attached' => $rows, 'family_id' => $familyId, 'user_id' => $userId]);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION C — Maintenance actions
|
||||
* =======================================================*/
|
||||
|
||||
// POST /families/set-primary-home
|
||||
// body: family_id, student_id, is_primary_home (0/1)
|
||||
public function setPrimaryHome()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
$flag = (int) $this->request->getPost('is_primary_home');
|
||||
|
||||
if (!$familyId || !$studentId) return $this->bad('family_id and student_id required');
|
||||
|
||||
$this->familyStudents
|
||||
->where(['family_id' => $familyId, 'student_id' => $studentId])
|
||||
->set(['is_primary_home' => $flag ? 1 : 0])
|
||||
->update();
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
// POST /families/set-guardian-flags
|
||||
// body: family_id, user_id, receive_emails(0/1), is_primary(0/1), receive_sms(0/1)
|
||||
public function setGuardianFlags()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
|
||||
if (!$familyId || !$userId) return $this->bad('family_id and user_id required');
|
||||
|
||||
$data = [];
|
||||
foreach (['receive_emails', 'is_primary', 'receive_sms', 'relation'] as $k) {
|
||||
if (null !== $this->request->getPost($k)) {
|
||||
$val = $this->request->getPost($k);
|
||||
$data[$k] = in_array($k, ['receive_emails', 'is_primary', 'receive_sms'])
|
||||
? (int)$val
|
||||
: (string)$val;
|
||||
}
|
||||
}
|
||||
if (!$data) return $this->bad('No flags provided');
|
||||
|
||||
$this->guardians->where(['family_id' => $familyId, 'user_id' => $userId])->set($data)->update();
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
// POST /families/unlink-guardian
|
||||
// body: family_id, user_id
|
||||
public function unlinkGuardian()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
if (!$familyId || !$userId) return $this->bad('family_id and user_id required');
|
||||
|
||||
$this->guardians->where(['family_id' => $familyId, 'user_id' => $userId])->delete();
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
// POST /families/unlink-student
|
||||
// body: family_id, student_id
|
||||
public function unlinkStudent()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
if (!$familyId || !$studentId) return $this->bad('family_id and student_id required');
|
||||
|
||||
$this->familyStudents->where(['family_id' => $familyId, 'student_id' => $studentId])->delete();
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION D — Private helpers
|
||||
* =======================================================*/
|
||||
|
||||
// Ensure there is exactly one family per primary parent (users.id)
|
||||
// Returns $familyId and increments $created by reference if newly created.
|
||||
protected function ensureFamilyForPrimaryParent(int $primaryUserId, int &$createdCounter): int
|
||||
{
|
||||
$code = 'FAM-' . $primaryUserId;
|
||||
$row = $this->families->where('family_code', $code)->first();
|
||||
if ($row) return (int)$row['id'];
|
||||
|
||||
$this->families->insert([
|
||||
'family_code' => $code,
|
||||
'household_name' => 'Family of User ' . $primaryUserId,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
$createdCounter++;
|
||||
return (int)$this->families->getInsertID();
|
||||
}
|
||||
|
||||
// Attach a student to a family (idempotent; returns rows affected >0 if inserted)
|
||||
protected function attachStudentToFamily(int $studentId, int $familyId): int
|
||||
{
|
||||
// INSERT IGNORE pattern
|
||||
$sql = "INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home)
|
||||
VALUES (?, ?, 1)";
|
||||
$this->db->query($sql, [$familyId, $studentId]);
|
||||
return $this->db->affectedRows();
|
||||
}
|
||||
|
||||
// Attach a guardian user to family (idempotent)
|
||||
protected function attachGuardianUser(
|
||||
int $familyId,
|
||||
int $userId,
|
||||
string $relation = 'primary',
|
||||
bool $isPrimary = false,
|
||||
int $receiveEmails = 1,
|
||||
int $receiveSms = 0
|
||||
): int {
|
||||
$sql = "INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms)
|
||||
VALUES (?, ?, ?, ?, ?, ?)";
|
||||
$this->db->query($sql, [$familyId, $userId, $relation, $isPrimary ? 1 : 0, $receiveEmails, $receiveSms]);
|
||||
return $this->db->affectedRows();
|
||||
}
|
||||
|
||||
// Find the “primary” family for a student = family created from parent_id
|
||||
protected function familyIdFromStudentPrimary(int $studentId): ?int
|
||||
{
|
||||
// 1) Get primary parent id for the student
|
||||
$st = $this->students->select('parent_id')->find($studentId);
|
||||
if (!$st || empty($st['parent_id'])) return null;
|
||||
|
||||
// 2) The canonical family uses code FAM-{parent_id}
|
||||
$code = 'FAM-' . (int)$st['parent_id'];
|
||||
$row = $this->families->select('id')->where('family_code', $code)->first();
|
||||
if ($row) return (int)$row['id'];
|
||||
|
||||
// If not found, fall back to any family linked already
|
||||
$row = $this->db->query(
|
||||
"SELECT family_id FROM family_students WHERE student_id = ? ORDER BY is_primary_home DESC LIMIT 1",
|
||||
[$studentId]
|
||||
)->getRowArray();
|
||||
|
||||
return $row ? (int)$row['family_id'] : null;
|
||||
}
|
||||
|
||||
// Simple 400
|
||||
protected function bad(string $msg)
|
||||
{
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 'error', 'message' => $msg]);
|
||||
}
|
||||
|
||||
// Example permission check hook (plug your own logic)
|
||||
protected function userCan(string $perm): bool
|
||||
{
|
||||
$perms = (array) (session('permissions') ?? []);
|
||||
return in_array($perm, $perms, true);
|
||||
}
|
||||
|
||||
protected function deny()
|
||||
{
|
||||
return redirect()->to('/access_denied')->setStatusCode(403);
|
||||
}
|
||||
|
||||
// Legacy import helper: map secondary parents from legacy 'parents' table
|
||||
public function importSecondParentsFromLegacy()
|
||||
{
|
||||
// Protect this route (e.g., admin only) via filter in routes
|
||||
$L = [
|
||||
'table' => 'parents',
|
||||
'firstparent_id' => 'firstparent_id', // users.id of primary (first) parent
|
||||
'second_user_id' => 'secondparent_id', // users.id of second parent (optional)
|
||||
'second_email' => 'secondparent_email',
|
||||
'second_firstname' => 'secondparent_firstname',
|
||||
'second_lastname' => 'secondparent_lastname',
|
||||
];
|
||||
|
||||
if (!$this->db->tableExists($L['table'])) {
|
||||
return $this->response->setJSON(['status' => 'error', 'message' => "Legacy table '{$L['table']}' not found"]);
|
||||
}
|
||||
foreach (['families','family_students','family_guardians'] as $t) {
|
||||
if (! $this->db->tableExists($t)) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => "Missing required table '{$t}'. Run migrations: php spark migrate",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $this->db->table($L['table'])
|
||||
->select(
|
||||
"{$L['firstparent_id']} AS first_id, " .
|
||||
"{$L['second_user_id']} AS second_id, " .
|
||||
"{$L['second_email']} AS email, " .
|
||||
"{$L['second_firstname']} AS firstname, " .
|
||||
"{$L['second_lastname']} AS lastname",
|
||||
false
|
||||
)
|
||||
->groupStart()
|
||||
->where("{$L['second_user_id']} !=", 0)
|
||||
->orWhere("{$L['second_email']} !=", '')
|
||||
->groupEnd()
|
||||
->get()->getResultArray();
|
||||
|
||||
$createdUsers = 0;
|
||||
$linked = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$firstId = (int)($r['first_id'] ?? 0);
|
||||
$secondId = (int)($r['second_id'] ?? 0);
|
||||
$email = trim((string)($r['email'] ?? ''));
|
||||
|
||||
if (!$firstId || (!$secondId && $email === '')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure/find family by primary parent
|
||||
$code = 'FAM-' . $firstId;
|
||||
$fam = $this->families->select('id')->where('family_code', $code)->first();
|
||||
if ($fam) {
|
||||
$familyId = (int)$fam['id'];
|
||||
} else {
|
||||
$tmp = 0; // counter sink
|
||||
$familyId = $this->ensureFamilyForPrimaryParent($firstId, $tmp);
|
||||
}
|
||||
|
||||
// Resolve/create user
|
||||
$userId = 0;
|
||||
if ($secondId > 0) {
|
||||
$userId = $secondId;
|
||||
} else {
|
||||
$user = $this->users->where('email', $email)->first();
|
||||
if (!$user) {
|
||||
$this->users->insert([
|
||||
'firstname' => $r['firstname'] ?? '',
|
||||
'lastname' => $r['lastname'] ?? '',
|
||||
'email' => $email,
|
||||
'status' => 'Active',
|
||||
'user_type' => 'secondary',
|
||||
]);
|
||||
$userId = (int)$this->users->getInsertID();
|
||||
$createdUsers++;
|
||||
} else {
|
||||
$userId = (int)$user['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all students for this primary parent are linked to this family
|
||||
$stuRows = $this->db->query("SELECT id FROM students WHERE parent_id = ?", [$firstId])->getResultArray();
|
||||
foreach ($stuRows as $sr) {
|
||||
$sid = (int)($sr['id'] ?? 0);
|
||||
if ($sid > 0) {
|
||||
$this->attachStudentToFamily($sid, $familyId);
|
||||
}
|
||||
}
|
||||
|
||||
// Link as guardian (idempotent)
|
||||
$linked += $this->attachGuardianUser($familyId, $userId, 'guardian', false, 1, 0);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'status' => 'ok',
|
||||
'created_users' => $createdUsers,
|
||||
'guardians_linked' => $linked,
|
||||
'skipped' => $skipped,
|
||||
'total_source' => count($rows),
|
||||
];
|
||||
|
||||
if (strtolower($this->request->getMethod()) === 'get') {
|
||||
$msg = json_encode($payload);
|
||||
return redirect()->to('/family')->with('message', "Import legacy result: {$msg}");
|
||||
}
|
||||
|
||||
return $this->response->setJSON($payload);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\SupplierModel;
|
||||
|
||||
class SupplierController extends BaseController
|
||||
{
|
||||
protected $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new SupplierModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$q = trim($this->request->getGet('q') ?? '');
|
||||
$builder = $this->model;
|
||||
if ($q !== '') {
|
||||
$builder = $builder->groupStart()
|
||||
->like('name', $q)->orLike('email', $q)->orLike('phone', $q)
|
||||
->groupEnd();
|
||||
}
|
||||
return view('inventory/suppliers_index', [
|
||||
'suppliers' => $builder->orderBy('name')->paginate(20),
|
||||
'pager' => $this->model->pager,
|
||||
'q' => $q,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('inventory/suppliers_form', [
|
||||
'title' => 'Add Supplier',
|
||||
'action' => site_url('inventory/suppliers/store'),
|
||||
'supplier' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$data = $this->request->getPost(['name','email','phone','address','notes']);
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/suppliers'))->with('success', 'Supplier saved.');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$s = $this->model->find($id);
|
||||
if (!$s) return redirect()->to('inventory/suppliers')->with('error', 'Not found.');
|
||||
return view('inventory/suppliers_form', [
|
||||
'title' => 'Edit Supplier',
|
||||
'action' => site_url('inventory/suppliers/update/'.$id),
|
||||
'supplier' => $s,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$data = $this->request->getPost(['name','email','phone','address','notes']);
|
||||
$data['id'] = $id;
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/suppliers'))->with('success', 'Supplier updated.');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->model->delete($id);
|
||||
return redirect()->to(site_url('inventory/suppliers'))->with('success', 'Supplier deleted.');
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\SupplyCategoryModel;
|
||||
|
||||
|
||||
class SupplyCategoryController extends BaseController
|
||||
{
|
||||
protected $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new SupplyCategoryModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('inventory/categories_index', [
|
||||
'categories' => $this->model->orderBy('name')->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('inventory/categories_form', [
|
||||
'title' => 'Add Category',
|
||||
'action' => site_url('inventory/categories/store'),
|
||||
'category' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$data = $this->request->getPost(['name']);
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/categories'))->with('success', 'Category saved.');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$cat = $this->model->find($id);
|
||||
if (!$cat) return redirect()->to('inventory/categories')->with('error', 'Not found.');
|
||||
return view('inventory/categories_form', [
|
||||
'title' => 'Edit Category',
|
||||
'action' => site_url('inventory/categories/update/'.$id),
|
||||
'category' => $cat,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$data = $this->request->getPost(['name']);
|
||||
$data['id'] = (int) $id;
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/categories'))->with('success', 'Category updated.');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->model->delete($id);
|
||||
return redirect()->to(site_url('inventory/categories'))->with('success', 'Category deleted.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user