1330 lines
55 KiB
PHP
1330 lines
55 KiB
PHP
<?php
|
||
|
||
namespace App\Controllers\View;
|
||
|
||
use App\Controllers\BaseController; // Models
|
||
use App\Models\StudentModel;
|
||
use App\Models\ClassSectionModel;
|
||
use App\Models\UserModel;
|
||
use App\Models\EnrollmentModel;
|
||
use App\Models\ParentModel;
|
||
use App\Models\ConfigurationModel;
|
||
use App\Models\WhatsappGroupLinkModel;
|
||
use App\Models\WhatsappInviteLogModel;
|
||
use App\Models\WhatsappGroupMembershipModel;
|
||
use CodeIgniter\Events\Events;
|
||
|
||
/**
|
||
* WhatsApp invites controller — rewritten to:
|
||
* - Build initial lists from student_class (sections) and users/parents (parents)
|
||
* - Parents mode: given a parent id, find their students -> classSectionIds -> links -> send
|
||
* - Class mode: given classSectionId, find students -> parents (primary+second) -> send link
|
||
* - All mode: iterate all classSectionIds in student_class, gather recipients -> send per parent
|
||
*/
|
||
class WhatsappController extends BaseController
|
||
{
|
||
protected $db;
|
||
protected $configModel;
|
||
protected $semester;
|
||
protected $schoolYear;
|
||
/** cache for presence of roster rows per term */
|
||
private array $rosterPresenceCache = [];
|
||
protected ParentModel $parentsModel;
|
||
protected StudentModel $studentModel;
|
||
protected ClassSectionModel $classSectionModel;
|
||
protected UserModel $userModel;
|
||
protected EnrollmentModel $enrollmentModel;
|
||
protected WhatsappGroupLinkModel $linkModel;
|
||
protected WhatsappInviteLogModel $logModel;
|
||
protected WhatsappGroupMembershipModel $membershipModel;
|
||
|
||
public function __construct()
|
||
{
|
||
helper('url');
|
||
// Load the database service
|
||
$this->db = \Config\Database::connect();
|
||
// Init models
|
||
$this->userModel = new UserModel();
|
||
$this->studentModel = new StudentModel();
|
||
$this->classSectionModel = new ClassSectionModel();
|
||
$this->linkModel = new WhatsappGroupLinkModel();
|
||
$this->membershipModel = new WhatsappGroupMembershipModel();
|
||
$this->configModel = new ConfigurationModel();
|
||
|
||
$this->semester = $this->configModel->getConfig('semester');
|
||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||
}
|
||
|
||
/**
|
||
* Admin page: pre-load ALL parents (primaries + second-parents) and ALL sections (from student_class).
|
||
*/
|
||
public function index()
|
||
{
|
||
// This page is year-only: ignore semester filters.
|
||
$semester = '';
|
||
[$parents, $sections] = $this->prepareIndexData($this->schoolYear, $semester);
|
||
|
||
// Build links map for small UI hints/badges
|
||
$links = $this->linkModel->getAllForTerm($this->schoolYear, $semester);
|
||
$linksBySection = [];
|
||
foreach ($links as $l) {
|
||
$cid = (int) ($l['class_section_id'] ?? 0);
|
||
if ($cid && !isset($linksBySection[$cid])) {
|
||
$linksBySection[$cid] = $l;
|
||
}
|
||
}
|
||
|
||
return view('whatsapp/manage_links', [
|
||
'sections' => $sections, // [ ['class_section_id','class_section_name'], ... ]
|
||
'parents' => $parents, // [ ['id','firstname','lastname','email','source'], ... ]
|
||
'linksBySection' => $linksBySection, // [class_section_id => linkRow]
|
||
'schoolYear' => $this->schoolYear,
|
||
'semester' => $semester,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Create/update a WhatsApp link for a class section.
|
||
*/
|
||
public function saveLink()
|
||
{
|
||
$sectionId = (int) $this->request->getPost('class_section_id');
|
||
$sectionName = (string) ($this->request->getPost('class_section_name') ?? '');
|
||
$inviteLink = (string) $this->request->getPost('invite_link');
|
||
$active = (int) ($this->request->getPost('active') ? 1 : 0);
|
||
|
||
if (!$sectionId || !$inviteLink) {
|
||
return redirect()->back()->with('error', 'Class and link are required.');
|
||
}
|
||
|
||
// Resolve display name if not provided
|
||
if ($sectionName === '') {
|
||
$row = $this->linkModel->getLinkForSection($sectionId, $this->schoolYear, $this->semester);
|
||
$sectionName = $row['class_section_name'] ?? ('Section ' . $sectionId);
|
||
}
|
||
|
||
$existing = $this->linkModel->where([
|
||
'class_section_id' => $sectionId,
|
||
'school_year' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
])->first();
|
||
|
||
$payload = [
|
||
'class_section_id' => $sectionId,
|
||
'class_section_name' => $sectionName,
|
||
'school_year' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
'invite_link' => trim($inviteLink),
|
||
'active' => $active,
|
||
];
|
||
|
||
if ($existing) {
|
||
$this->linkModel->update($existing['id'], $payload);
|
||
} else {
|
||
$this->linkModel->insert($payload);
|
||
}
|
||
|
||
return redirect()->back()->with('success', 'WhatsApp group link saved.');
|
||
}
|
||
|
||
/**
|
||
* Send invites in three modes: 'parents' | 'class' | 'all'
|
||
* POST fields:
|
||
* - mode: 'parents' | 'class' | 'all'
|
||
* - class_section_id: int (required for mode='class')
|
||
* - parent_ids[]: array<int> (required for mode='parents'; can be primary users.id or parents.id)
|
||
*/
|
||
// Controller method
|
||
public function sendInvites()
|
||
{
|
||
// This page is year-only: ignore semester filters.
|
||
$semester = '';
|
||
$mode = (string) trim((string) $this->request->getPost('mode'));
|
||
$parentIdsPicked = array_map('intval', (array) ($this->request->getPost('parent_ids') ?? []));
|
||
$rawClassId = $this->request->getPost('class_section_id');
|
||
$classSectionId = ($rawClassId === '' || $rawClassId === null) ? 0 : (int) $rawClassId;
|
||
|
||
if (!in_array($mode, ['parents', 'class', 'all'], true)) {
|
||
return redirect()->back()->with('error', 'Invalid mode.');
|
||
}
|
||
if ($mode === 'parents' && empty(array_filter($parentIdsPicked))) {
|
||
return redirect()->back()->with('error', 'Please select at least one parent.');
|
||
}
|
||
if ($mode === 'class' && $classSectionId <= 0) {
|
||
return redirect()->back()->with('error', 'Please select a class/section.');
|
||
}
|
||
|
||
// Load all links for the term once
|
||
$links = $this->linkModel->getAllForTerm($this->schoolYear, $semester);
|
||
$linkBySection = [];
|
||
foreach ($links as $l) {
|
||
$cid = (int) ($l['class_section_id'] ?? 0);
|
||
if ($cid && !isset($linkBySection[$cid])) {
|
||
$linkBySection[$cid] = $l;
|
||
}
|
||
}
|
||
|
||
// Build per-family bundles
|
||
$payloads = [];
|
||
if ($mode === 'parents') {
|
||
foreach ($parentIdsPicked as $pickedId) {
|
||
if ($pickedId <= 0) continue;
|
||
$bundle = $this->bundleForSingleParent($pickedId, $linkBySection);
|
||
if ($bundle && !empty($bundle['emails']) && !empty($bundle['links'])) {
|
||
$payloads[] = $bundle;
|
||
}
|
||
}
|
||
} elseif ($mode === 'class') {
|
||
$bundles = $this->bundleForSingleClass($classSectionId, $linkBySection); // per-parent bundles
|
||
foreach ($bundles as $b) {
|
||
if (!empty($b['emails']) && !empty($b['links'])) {
|
||
$payloads[] = $b;
|
||
}
|
||
}
|
||
} else { // 'all'
|
||
$bundles = $this->bundlesForAllParentsAllClasses($linkBySection); // per-parent bundles across all
|
||
foreach ($bundles as $b) {
|
||
if (!empty($b['emails']) && !empty($b['links'])) {
|
||
$payloads[] = $b;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($payloads)) {
|
||
return redirect()->back()->with('error', 'No recipients found (missing links or emails).');
|
||
}
|
||
|
||
/**
|
||
* Consolidate per-student bundles into one bundle per parent.
|
||
* Key priority: parent id; fallback: normalized primary emails joined.
|
||
*/
|
||
$normEmailsList = static function (array $list): array {
|
||
$out = [];
|
||
foreach ($list as $e) {
|
||
$e = strtolower(trim((string) $e));
|
||
if ($e !== '' && filter_var($e, FILTER_VALIDATE_EMAIL)) {
|
||
$out[$e] = true;
|
||
}
|
||
}
|
||
return array_keys($out);
|
||
};
|
||
|
||
$mergeUniqueList = static function (array $a, array $b): array {
|
||
// preserves scalar lists, case-sensitive for display fields
|
||
$map = [];
|
||
foreach (array_merge($a, $b) as $v) {
|
||
$k = is_array($v) ? md5(json_encode($v)) : (string) $v;
|
||
$map[$k] = $v;
|
||
}
|
||
return array_values($map);
|
||
};
|
||
|
||
/**
|
||
* Merge "sections" as associative rows; prefer dedupe by class_section_id if present,
|
||
* otherwise by name string.
|
||
*/
|
||
$mergeSections = static function (array $A, array $B): array {
|
||
$byKey = [];
|
||
$put = static function (&$byKey, $row) {
|
||
if (is_array($row)) {
|
||
$id = $row['class_section_id'] ?? null;
|
||
$name = $row['class_section_name'] ?? null;
|
||
$key = $id !== null ? ('id:' . (int)$id) : ('name:' . (string)$name);
|
||
$byKey[$key] = $row;
|
||
} else {
|
||
// string fallback
|
||
$byKey['name:' . (string)$row] = $row;
|
||
}
|
||
};
|
||
foreach ($A as $r) $put($byKey, $r);
|
||
foreach ($B as $r) $put($byKey, $r);
|
||
return array_values($byKey);
|
||
};
|
||
|
||
/**
|
||
* Merge "links" items. Accepts mixed shapes:
|
||
* - array of link items (recommended): ['class_section_id','class_section_name','invite_link','qr_src']
|
||
* - string URL(s) (legacy)
|
||
*/
|
||
$mergeLinks = static function (array $A, array $B): array {
|
||
$index = [];
|
||
$add = static function (&$index, $item) {
|
||
if (is_array($item)) {
|
||
$key =
|
||
(isset($item['class_section_id']) && $item['class_section_id'] !== null)
|
||
? 'id:' . (int)$item['class_section_id']
|
||
: ('url:' . (string)($item['invite_link'] ?? json_encode($item)));
|
||
$index[$key] = $item;
|
||
} else {
|
||
$index['url:' . (string)$item] = $item;
|
||
}
|
||
};
|
||
foreach ($A as $it) $add($index, $it);
|
||
foreach ($B as $it) $add($index, $it);
|
||
return array_values($index);
|
||
};
|
||
|
||
$grouped = [];
|
||
foreach ($payloads as $b) {
|
||
$parent = $b['parent'] ?? [];
|
||
$pid = (int)($parent['id'] ?? 0);
|
||
$primary = $normEmailsList((array)($b['emails'] ?? []));
|
||
$groupKey = $pid > 0 ? ('pid:' . $pid) : ('mail:' . implode(',', $primary));
|
||
|
||
if (!isset($grouped[$groupKey])) {
|
||
$grouped[$groupKey] = [
|
||
'parent' => $parent,
|
||
'emails' => $primary,
|
||
'sections' => (array)($b['sections'] ?? []),
|
||
'links' => (array)($b['links'] ?? []),
|
||
'teachers' => (array)($b['teachers'] ?? []),
|
||
// Keep any "items" shape if your listener/view uses it
|
||
'items' => (array)($b['items'] ?? []),
|
||
];
|
||
} else {
|
||
// Merge
|
||
$grouped[$groupKey]['parent'] = !empty($parent) ? $parent : $grouped[$groupKey]['parent'];
|
||
$grouped[$groupKey]['emails'] = $normEmailsList(array_merge($grouped[$groupKey]['emails'], $primary));
|
||
$grouped[$groupKey]['sections'] = $mergeSections($grouped[$groupKey]['sections'], (array)($b['sections'] ?? []));
|
||
$grouped[$groupKey]['links'] = $mergeLinks($grouped[$groupKey]['links'], (array)($b['links'] ?? []));
|
||
$grouped[$groupKey]['teachers'] = $mergeUniqueList($grouped[$groupKey]['teachers'], (array)($b['teachers'] ?? []));
|
||
$grouped[$groupKey]['items'] = $mergeLinks($grouped[$groupKey]['items'], (array)($b['items'] ?? [])); // treat items like links
|
||
}
|
||
}
|
||
|
||
// Final consolidated list
|
||
$payloads = array_values($grouped);
|
||
|
||
|
||
// Ensure a listener is actually registered; otherwise nothing will fire.
|
||
$listenerCount = method_exists(\CodeIgniter\Events\Events::class, 'listeners')
|
||
? count(\CodeIgniter\Events\Events::listeners('whatsapp_invites.send'))
|
||
: -1;
|
||
log_message('debug', 'whatsapp_invites.send listener count: {c}', ['c' => $listenerCount]);
|
||
if ($listenerCount === 0) {
|
||
return redirect()->back()->with(
|
||
'error',
|
||
'No event listener registered for whatsapp_invites.send. Add one in app/Config/Events.php.'
|
||
);
|
||
}
|
||
|
||
// Helpers
|
||
$norm = static function (array $list): array {
|
||
$out = [];
|
||
foreach ($list as $e) {
|
||
$e = strtolower(trim((string) $e));
|
||
if ($e !== '' && filter_var($e, FILTER_VALIDATE_EMAIL)) {
|
||
$out[$e] = true;
|
||
}
|
||
}
|
||
return array_keys($out);
|
||
};
|
||
|
||
// Extract secondary emails from bundle shapes + DB fallback
|
||
$getSecondaryEmails = function (array $bundle) use ($norm) {
|
||
$collected = [];
|
||
|
||
// Explicit arrays from bundlers
|
||
if (!empty($bundle['emails_secondary'])) {
|
||
$collected = array_merge($collected, (array) $bundle['emails_secondary']);
|
||
}
|
||
if (!empty($bundle['secondary_parents'])) {
|
||
foreach ((array) $bundle['secondary_parents'] as $sp) {
|
||
if (!empty($sp['email'])) $collected[] = $sp['email'];
|
||
}
|
||
}
|
||
|
||
// Common single-field shapes on parent
|
||
$p = $bundle['parent'] ?? [];
|
||
foreach (['secondary_email', 'secondparent_email', 'sec_email', 'email_secondary'] as $k) {
|
||
if (!empty($p[$k])) $collected[] = $p[$k];
|
||
}
|
||
if (!empty($p['secondparent']['email'])) $collected[] = $p['secondparent']['email'];
|
||
|
||
// DB fallback by firstparent_id == primary parent id
|
||
try {
|
||
$primaryId = (int) ($p['id'] ?? 0);
|
||
if ($primaryId > 0) {
|
||
$row = $this->db->table('parents')
|
||
->select('secondparent_email')
|
||
->where('firstparent_id', $primaryId)
|
||
->orderBy('updated_at', 'DESC')
|
||
->get()->getFirstRow('array');
|
||
if (!empty($row['secondparent_email'])) {
|
||
$collected[] = $row['secondparent_email'];
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'Secondary email DB lookup failed: {err}', ['err' => $e->getMessage()]);
|
||
}
|
||
|
||
return $norm($collected);
|
||
};
|
||
|
||
// Trigger one event per family: TO = primary; CC = secondary
|
||
$triggered = 0;
|
||
|
||
foreach ($payloads as $b) {
|
||
try {
|
||
$primaryEmails = $norm((array) ($b['emails'] ?? []));
|
||
$secondaryEmails = $getSecondaryEmails($b);
|
||
|
||
if (empty($secondaryEmails)) {
|
||
log_message('debug', 'No secondary_emails for parent_id={pid}; bundle keys={keys}', [
|
||
'pid' => $b['parent']['id'] ?? null,
|
||
'keys' => implode(',', array_keys($b)),
|
||
]);
|
||
}
|
||
|
||
$to = $primaryEmails;
|
||
$cc = [];
|
||
|
||
if (!empty($primaryEmails) && !empty($secondaryEmails)) {
|
||
// keep CC distinct from TO
|
||
$cc = array_values(array_diff($secondaryEmails, $primaryEmails));
|
||
} elseif (empty($primaryEmails) && !empty($secondaryEmails)) {
|
||
$to = $secondaryEmails; // only secondary exists → send to them
|
||
}
|
||
|
||
if (empty($to)) {
|
||
log_message('warning', 'Skipping whatsapp_invites.send: no recipient email(s) for parent_id {pid}', [
|
||
'pid' => $b['parent']['id'] ?? null
|
||
]);
|
||
continue;
|
||
}
|
||
|
||
\CodeIgniter\Events\Events::trigger('whatsapp_invites.send', [
|
||
'parent' => $b['parent'] ?? null,
|
||
'sections' => $b['sections'] ?? [],
|
||
'schoolYear' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
'links' => $b['links'] ?? [],
|
||
'teachers' => $b['teachers'] ?? [],
|
||
'to_emails' => $to,
|
||
'cc_emails' => $cc,
|
||
]);
|
||
|
||
$triggered++;
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'whatsapp_invites.send failed: {err}', ['err' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
return redirect()->back()->with('success', "Invite processing started for {$triggered} recipient group(s).");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Internal helpers (new logic)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
public function parentContacts()
|
||
{
|
||
// Term context (use controller properties if you already set them)
|
||
$schoolYear = $this->schoolYear ?? (string)($this->request->getGet('school_year') ?? '');
|
||
$semester = $this->semester ?? (string)($this->request->getGet('semester') ?? '');
|
||
|
||
// parents = term-scoped table holding second parent fields + firstparent_id
|
||
// users = primary parent records
|
||
$b = $this->db->table('parents sp');
|
||
|
||
$b->select("
|
||
sp.id AS parents_row_id,
|
||
sp.firstparent_id AS firstparent_id,
|
||
sp.secondparent_firstname AS sp_firstname,
|
||
sp.secondparent_lastname AS sp_lastname,
|
||
sp.secondparent_email AS sp_email,
|
||
sp.secondparent_phone AS sp_phone,
|
||
sp.school_year AS sp_school_year,
|
||
sp.semester AS sp_semester,
|
||
|
||
u.id AS u_id,
|
||
u.firstname AS u_firstname,
|
||
u.lastname AS u_lastname,
|
||
u.email AS u_email,
|
||
u.cellphone AS u_phone,
|
||
u.school_year AS u_school_year,
|
||
u.semester AS u_semester
|
||
");
|
||
|
||
$b->join('users u', 'u.id = sp.firstparent_id', 'left');
|
||
|
||
// Scope by term using parents table (authoritative for the pairing)
|
||
if ($schoolYear !== '') {
|
||
$b->where('sp.school_year', $schoolYear);
|
||
}
|
||
if ($semester !== '') {
|
||
$b->where('sp.semester', $semester);
|
||
}
|
||
|
||
$rows = $b->get()->getResultArray();
|
||
|
||
// Flatten into a single contacts list (primary + second parent as separate rows)
|
||
$contacts = [];
|
||
foreach ($rows as $r) {
|
||
// 1) Primary (first) parent from users
|
||
if (!empty($r['u_id'])) {
|
||
$contacts[] = [
|
||
'firstname' => (string)($r['u_firstname'] ?? ''),
|
||
'lastname' => (string)($r['u_lastname'] ?? ''),
|
||
'email' => (string)($r['u_email'] ?? ''),
|
||
'phone' => (string)($r['u_phone'] ?? ''),
|
||
'type' => 'Primary Parent',
|
||
];
|
||
}
|
||
|
||
// 2) Second parent from parents (only if we have at least a name/phone/email)
|
||
$hasSecond =
|
||
!empty($r['sp_firstname']) ||
|
||
!empty($r['sp_lastname']) ||
|
||
!empty($r['sp_email']) ||
|
||
!empty($r['sp_phone']);
|
||
|
||
if ($hasSecond) {
|
||
$contacts[] = [
|
||
'firstname' => (string)($r['sp_firstname'] ?? ''),
|
||
'lastname' => (string)($r['sp_lastname'] ?? ''),
|
||
'email' => (string)($r['sp_email'] ?? ''),
|
||
'phone' => (string)($r['sp_phone'] ?? ''),
|
||
'type' => 'Second Parent',
|
||
];
|
||
}
|
||
}
|
||
|
||
// Optional: remove totally empty lines (no name + no contact)
|
||
$contacts = array_values(array_filter($contacts, function ($c) {
|
||
$name = trim(($c['firstname'] ?? '') . ($c['lastname'] ?? ''));
|
||
$contact = trim(($c['phone'] ?? '') . ($c['email'] ?? ''));
|
||
return $name !== '' || $contact !== '';
|
||
}));
|
||
|
||
// Sort by last name, first name, then type (Primary before Second)
|
||
usort($contacts, function ($a, $b) {
|
||
$la = strtolower($a['lastname'] ?? '');
|
||
$lb = strtolower($b['lastname'] ?? '');
|
||
if ($la !== $lb) return $la <=> $lb;
|
||
$fa = strtolower($a['firstname'] ?? '');
|
||
$fb = strtolower($b['firstname'] ?? '');
|
||
if ($fa !== $fb) return $fa <=> $fb;
|
||
// Primary before Second
|
||
$ta = ($a['type'] ?? '') === 'Primary Parent' ? 0 : 1;
|
||
$tb = ($b['type'] ?? '') === 'Primary Parent' ? 0 : 1;
|
||
return $ta <=> $tb;
|
||
});
|
||
|
||
return view('whatsapp/parent_contacts', [
|
||
'contacts' => array_map(function ($c) {
|
||
// The view expects 'phone' and 'email' directly; keep as-is
|
||
return [
|
||
'firstname' => $c['firstname'],
|
||
'lastname' => $c['lastname'],
|
||
'email' => $c['email'],
|
||
'phone' => $c['phone'],
|
||
'type' => $c['type'],
|
||
];
|
||
}, $contacts),
|
||
'schoolYear' => $schoolYear,
|
||
'semester' => $semester,
|
||
]);
|
||
}
|
||
|
||
public function parentContactsByClass()
|
||
{
|
||
$schoolYear = $this->schoolYear ?? (string)($this->request->getGet('school_year') ?? '');
|
||
// This page is year-only: ignore semester filters.
|
||
$semester = '';
|
||
|
||
// 1) Get sections actually used this term from student_class, then name from classSection
|
||
// 1) Get distinct class_section_id actually used this term from student_class
|
||
$csb = $this->db->table('student_class sc')
|
||
->select('sc.class_section_id, MAX(sc.school_year) AS school_year');
|
||
|
||
if ($schoolYear !== '') $csb->where('sc.school_year', $schoolYear);
|
||
|
||
$sections = $csb->groupBy('sc.class_section_id')
|
||
->orderBy('sc.class_section_id', 'ASC')
|
||
->get()->getResultArray();
|
||
|
||
// Build list of IDs
|
||
$sectionIds = array_values(array_filter(array_unique(array_map(
|
||
fn($r) => (int)($r['class_section_id'] ?? 0),
|
||
$sections
|
||
))));
|
||
|
||
// 2) Fetch names from classSection in a separate query and map by id
|
||
$namesById = [];
|
||
if (!empty($sectionIds)) {
|
||
$nb = $this->db->table('classSection') // IMPORTANT: exact table name/case
|
||
->select('class_section_id, class_section_name')
|
||
->whereIn('class_section_id', $sectionIds)
|
||
->get()->getResultArray();
|
||
|
||
foreach ($nb as $row) {
|
||
$namesById[(int)$row['class_section_id']] = (string)($row['class_section_name'] ?? '');
|
||
}
|
||
}
|
||
|
||
// 3) Build $classSections with reliable names
|
||
$classSections = [];
|
||
foreach ($sections as $s) {
|
||
$sid = (int)($s['class_section_id'] ?? 0);
|
||
if (!$sid) continue;
|
||
|
||
$classSections[$sid] = [
|
||
'class_section_id' => $sid,
|
||
'class_section_name' => $namesById[$sid] ?? ('Section ' . $sid),
|
||
'semester' => $semester,
|
||
'school_year' => (string)($s['school_year'] ?? ''),
|
||
'description' => '',
|
||
'main_teachers' => [],
|
||
'teacher_assistants' => [],
|
||
'parents' => [],
|
||
];
|
||
}
|
||
|
||
|
||
// 2) Parents per class:
|
||
// sc → students s → users u (primary parent)
|
||
// parents sp on (sp.firstparent_id = u.id) for second parent (same term)
|
||
$pb = $this->db->table('student_class sc');
|
||
$pb->select("
|
||
sc.class_section_id,
|
||
|
||
u.id AS primary_id,
|
||
u.firstname AS primary_firstname,
|
||
u.lastname AS primary_lastname,
|
||
u.email AS primary_email,
|
||
u.cellphone AS primary_phone,
|
||
|
||
sp.id AS second_id,
|
||
sp.secondparent_firstname AS second_firstname,
|
||
sp.secondparent_lastname AS second_lastname,
|
||
sp.secondparent_email AS second_email,
|
||
sp.secondparent_phone AS second_phone
|
||
");
|
||
|
||
// IMPORTANT: join through students table (NOT sc.parent_id)
|
||
$pb->join('students s', 's.id = sc.student_id', 'inner'); // adjust if your table is named `student` (singular)
|
||
$pb->join('users u', 'u.id = s.parent_id', 'inner'); // primary parent lives on students.parent_id
|
||
|
||
// Second parent row for the same term (allow NULL/'' semester if you sometimes omit it)
|
||
$pb->join(
|
||
'parents sp',
|
||
"sp.firstparent_id = u.id
|
||
AND sp.school_year = sc.school_year",
|
||
'left'
|
||
);
|
||
|
||
if ($schoolYear !== '') $pb->where('sc.school_year', $schoolYear);
|
||
|
||
$pb->orderBy('sc.class_section_id', 'ASC')
|
||
->orderBy('u.lastname', 'ASC')
|
||
->orderBy('u.firstname', 'ASC');
|
||
|
||
$rows = $pb->get()->getResultArray();
|
||
|
||
// 3) De-duplicate a primary parent within the same class section
|
||
$seen = [];
|
||
foreach ($rows as $r) {
|
||
$sid = (int)($r['class_section_id'] ?? 0);
|
||
$pid = (int)($r['primary_id'] ?? 0);
|
||
if (!$sid || !$pid || !isset($classSections[$sid])) continue;
|
||
|
||
$key = $sid . ':' . $pid;
|
||
if (isset($seen[$key])) continue;
|
||
$seen[$key] = true;
|
||
|
||
$classSections[$sid]['parents'][] = [
|
||
'class_section_id' => $sid,
|
||
'primary_id' => $pid,
|
||
'primary_name' => trim(($r['primary_lastname'] ?? '') . ', ' . ($r['primary_firstname'] ?? '')),
|
||
'primary_phone' => (string)($r['primary_phone'] ?? ''),
|
||
'primary_email' => (string)($r['primary_email'] ?? ''),
|
||
'second_id' => (int)($r['second_id'] ?? 0),
|
||
'second_name' => trim(($r['second_lastname'] ?? '') . ', ' . ($r['second_firstname'] ?? '')),
|
||
'second_phone' => (string)($r['second_phone'] ?? ''),
|
||
'second_email' => (string)($r['second_email'] ?? ''),
|
||
// Use null to indicate "unknown/not recorded" so UI can show — (like attendance)
|
||
'primary_member' => null,
|
||
'second_member' => null,
|
||
];
|
||
}
|
||
|
||
// Annotate membership flags from whatsapp_group_memberships
|
||
try {
|
||
$sectionIds = array_keys($classSections);
|
||
$m = $this->membershipModel->getBySectionsAndTerm($sectionIds, $schoolYear, $semester);
|
||
foreach ($classSections as $sid => &$sec) {
|
||
foreach ($sec['parents'] as &$p) {
|
||
$pk1 = sprintf('%d:primary:%d', (int)$p['class_section_id'], (int)$p['primary_id']);
|
||
if (isset($m[$pk1])) {
|
||
$p['primary_member'] = (int)($m[$pk1]['is_member'] ?? 0);
|
||
}
|
||
$secondId = (int)($p['second_id'] ?? 0);
|
||
if ($secondId > 0) {
|
||
$pk2 = sprintf('%d:second:%d', (int)$p['class_section_id'], $secondId);
|
||
if (isset($m[$pk2])) {
|
||
$p['second_member'] = (int)($m[$pk2]['is_member'] ?? 0);
|
||
}
|
||
}
|
||
}
|
||
unset($p);
|
||
}
|
||
unset($sec);
|
||
} catch (\Throwable $e) {
|
||
// ignore membership annotation errors
|
||
}
|
||
|
||
// 4) (Optional) Teachers, if you keep them in teacher_class
|
||
try {
|
||
$tb = $this->db->table('teacher_class tc')
|
||
->select('tc.class_section_id, tc.position, u.firstname, u.lastname')
|
||
->join('users u', 'u.id = tc.teacher_id', 'left');
|
||
|
||
if ($schoolYear !== '') $tb->where('tc.school_year', $schoolYear);
|
||
|
||
$teachers = $tb->get()->getResultArray();
|
||
foreach ($teachers as $t) {
|
||
$sid = (int)($t['class_section_id'] ?? 0);
|
||
if (!isset($classSections[$sid])) continue;
|
||
$name = trim(($t['lastname'] ?? '') . ', ' . ($t['firstname'] ?? ''));
|
||
$pos = strtolower((string)($t['position'] ?? 'main'));
|
||
if ($pos === 'ta' || $pos === 'assistant') {
|
||
$classSections[$sid]['teacher_assistants'][] = $name;
|
||
} else {
|
||
$classSections[$sid]['main_teachers'][] = $name;
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// ignore if table not present
|
||
}
|
||
|
||
foreach ($classSections as &$sec) {
|
||
$sec['main_teachers'] = array_values(array_filter($sec['main_teachers'] ?? []));
|
||
$sec['teacher_assistants'] = array_values(array_filter($sec['teacher_assistants'] ?? []));
|
||
$sec['parents'] = array_values($sec['parents'] ?? []);
|
||
}
|
||
unset($sec);
|
||
|
||
return view('whatsapp/parent_contacts_by_class', [
|
||
'classSections' => array_values($classSections),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* POST: Update WhatsApp group membership flags for a class/parent(s).
|
||
* Accepts fields:
|
||
* - class_section_id (int)
|
||
* - primary_id (int, optional)
|
||
* - second_id (int, optional)
|
||
* - primary_member ('on' or '1' if checked)
|
||
* - second_member ('on' or '1' if checked)
|
||
*/
|
||
public function updateMembership()
|
||
{
|
||
// Only accept POST for writes
|
||
if (strtoupper($this->request->getMethod()) !== 'POST') {
|
||
return $this->response->setStatusCode(405)->setBody('Method Not Allowed');
|
||
}
|
||
|
||
// Read POSTed values explicitly (avoid GET bleed-through)
|
||
$sid = (int) ($this->request->getPost('class_section_id') ?? 0);
|
||
$primaryId = (int) ($this->request->getPost('primary_id') ?? 0);
|
||
$secondId = (int) ($this->request->getPost('second_id') ?? 0);
|
||
|
||
// Handle checkbox values that may arrive as string or array (if duplicated inputs)
|
||
$isChecked = static function ($v): bool {
|
||
if (is_array($v)) {
|
||
foreach ($v as $one) {
|
||
if ($one === '1' || strtolower((string)$one) === 'on') return true;
|
||
}
|
||
return false;
|
||
}
|
||
return ($v === '1' || strtolower((string)$v) === 'on');
|
||
};
|
||
$primaryValueRaw = $this->request->getPost('primary_member');
|
||
$secondValueRaw = $this->request->getPost('second_member');
|
||
|
||
// Debug trace (temporary): log incoming payload to help diagnose saving issues
|
||
try {
|
||
log_message('error', 'DBG updateMembership: method={m} sid={sid} pid={pid} sec={sec} pRaw={p} sRaw={sr}', [
|
||
'm' => $this->request->getMethod(),
|
||
'sid' => $sid,
|
||
'pid' => $primaryId,
|
||
'sec' => $secondId,
|
||
'p' => json_encode($primaryValueRaw),
|
||
'sr' => json_encode($secondValueRaw),
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
// ignore
|
||
}
|
||
|
||
if ($sid <= 0) {
|
||
return redirect()->back()->with('error', 'Invalid class section.');
|
||
}
|
||
|
||
$schoolYear = (string) ($this->request->getPost('school_year') ?? $this->schoolYear ?? '');
|
||
$semester = (string) ($this->request->getPost('semester') ?? $this->semester ?? '');
|
||
$schoolYear = trim($schoolYear);
|
||
$semester = trim($semester);
|
||
|
||
// Identify the actor (if available)
|
||
$verifiedBy = null;
|
||
try {
|
||
$verifiedBy = (int) (session('id') ?? 0) ?: null;
|
||
} catch (\Throwable $e) {
|
||
$verifiedBy = null;
|
||
}
|
||
|
||
// Ensure the target table exists (prefer tableExists if available)
|
||
$tableExists = null;
|
||
try {
|
||
if (method_exists($this->db, 'tableExists')) {
|
||
$tableExists = $this->db->tableExists('whatsapp_group_memberships');
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$tableExists = null; // fall back to getFieldNames below
|
||
}
|
||
if ($tableExists === null) {
|
||
try {
|
||
$this->db->getFieldNames('whatsapp_group_memberships');
|
||
$tableExists = true;
|
||
} catch (\Throwable $e) {
|
||
$tableExists = false;
|
||
}
|
||
}
|
||
if (!$tableExists) {
|
||
$msg = 'Membership table missing. Please run migrations.';
|
||
log_message('error', 'updateMembership aborted: {msg}', ['msg' => $msg]);
|
||
if ($this->request->isAJAX()) {
|
||
return $this->response->setStatusCode(500)->setJSON([
|
||
'status' => 'error',
|
||
'error' => $msg,
|
||
]);
|
||
}
|
||
return redirect()->back()->with('error', $msg);
|
||
}
|
||
|
||
try {
|
||
$saved = [];
|
||
if ($primaryId > 0) {
|
||
$isMember = $isChecked($primaryValueRaw);
|
||
$id = $this->membershipModel->upsertMembership($sid, $schoolYear, $semester, 'primary', $primaryId, $isMember, $verifiedBy);
|
||
if ($id <= 0) {
|
||
// Log DB error details if available
|
||
try {
|
||
$dberr = isset($this->membershipModel->db) ? $this->membershipModel->db->error() : [];
|
||
log_message('error', 'Failed to upsert primary membership: sid={sid} pid={pid} term={y}/{s} err={e}', [
|
||
'sid' => $sid, 'pid' => $primaryId, 'y' => $schoolYear, 's' => $semester, 'e' => json_encode($dberr)
|
||
]);
|
||
} catch (\Throwable $e) {}
|
||
}
|
||
$saved[] = 'primary=' . $primaryId . ':' . ($isMember ? '1' : '0');
|
||
}
|
||
if ($secondId > 0) {
|
||
$isMember = $isChecked($secondValueRaw);
|
||
$id = $this->membershipModel->upsertMembership($sid, $schoolYear, $semester, 'second', $secondId, $isMember, $verifiedBy);
|
||
if ($id <= 0) {
|
||
try {
|
||
$dberr = isset($this->membershipModel->db) ? $this->membershipModel->db->error() : [];
|
||
log_message('error', 'Failed to upsert second membership: sid={sid} sec={sec} term={y}/{s} err={e}', [
|
||
'sid' => $sid, 'sec' => $secondId, 'y' => $schoolYear, 's' => $semester, 'e' => json_encode($dberr)
|
||
]);
|
||
} catch (\Throwable $e) {}
|
||
}
|
||
$saved[] = 'second=' . $secondId . ':' . ($isMember ? '1' : '0');
|
||
}
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'Failed to update WhatsApp membership: {err}', ['err' => $e->getMessage()]);
|
||
if ($this->request->isAJAX()) {
|
||
return $this->response->setStatusCode(500)->setJSON([
|
||
'status' => 'error',
|
||
'error' => 'Failed to save membership.',
|
||
]);
|
||
}
|
||
return redirect()->back()->with('error', 'Failed to save membership.');
|
||
}
|
||
|
||
if (empty($saved)) {
|
||
if ($this->request->isAJAX()) {
|
||
return $this->response->setJSON([
|
||
'status' => 'noop',
|
||
'message' => 'Nothing to save (missing parent IDs).',
|
||
]);
|
||
}
|
||
return redirect()->back()->with('error', 'Nothing to save (missing parent IDs).');
|
||
}
|
||
|
||
if ($this->request->isAJAX()) {
|
||
return $this->response->setJSON([
|
||
'status' => 'ok',
|
||
'saved' => $saved,
|
||
'term' => ['school_year' => $schoolYear, 'semester' => $semester],
|
||
]);
|
||
}
|
||
|
||
return redirect()->back()->with('message', 'Membership saved: ' . implode(', ', $saved));
|
||
}
|
||
|
||
|
||
/**
|
||
* Build initial lists for the page:
|
||
* - ALL sections from student_class for current term
|
||
* - ALL parents: primaries (users who have students in term) + second parents (parents table)
|
||
*/
|
||
private function prepareIndexData(string $schoolYear, string $semester): array
|
||
{
|
||
$schoolYear = (string) $schoolYear;
|
||
$semester = trim((string) $semester);
|
||
[$yearAliases, $normalizedYear, $startYear] = $this->syAliases($schoolYear);
|
||
$semAliases = $semester !== '' ? $this->semAliases($semester) : [];
|
||
$limitToSemester = $semester !== '' && $this->hasRosterForTerm($schoolYear, $semester);
|
||
|
||
// -----------------------------
|
||
// Sections list (primary source: student_class)
|
||
// -----------------------------
|
||
$secQ = $this->db->table('student_class sc')
|
||
->select('DISTINCT sc.class_section_id', false);
|
||
|
||
// school_year filter with aliases
|
||
$secQ->groupStart()
|
||
->whereIn('sc.school_year', $yearAliases)
|
||
->orWhere("REPLACE(REPLACE(sc.school_year,'/','-'),'–','-')", $normalizedYear)
|
||
->orWhere('sc.school_year', (string)$startYear)
|
||
->groupEnd();
|
||
|
||
$secRows = $secQ->orderBy('sc.class_section_id', 'ASC')->get()->getResultArray();
|
||
|
||
// Fallback: if student_class is empty for the term, try enrollments
|
||
if (empty($secRows)) {
|
||
$secQ = $this->db->table('enrollments e')
|
||
->select('DISTINCT e.class_section_id', false);
|
||
|
||
$secQ->groupStart()
|
||
->whereIn('e.school_year', $yearAliases)
|
||
->orWhere("REPLACE(REPLACE(e.school_year,'/','-'),'–','-')", $normalizedYear)
|
||
->orWhere('e.school_year', (string)$startYear)
|
||
->groupEnd();
|
||
|
||
if ($limitToSemester && !empty($semAliases)) {
|
||
$secQ->groupStart();
|
||
foreach ($semAliases as $i => $s) {
|
||
$i === 0
|
||
? $secQ->where('LOWER(e.semester)', strtolower($s))
|
||
: $secQ->orWhere('LOWER(e.semester)', strtolower($s));
|
||
}
|
||
$secQ->orWhere('e.semester', $semester)->groupEnd();
|
||
} elseif ($limitToSemester) {
|
||
$secQ->where('e.semester', $semester);
|
||
}
|
||
|
||
$secRows = $secQ->orderBy('e.class_section_id', 'ASC')->get()->getResultArray();
|
||
}
|
||
|
||
$sections = [];
|
||
if (!empty($secRows)) {
|
||
foreach ($secRows as $r) {
|
||
$cid = (int)($r['class_section_id'] ?? 0);
|
||
if (!$cid) continue;
|
||
|
||
// Try to get a friendly name from links; fallback to "Section {id}"
|
||
$linkRow = $this->linkModel->getLinkForSection($cid, $schoolYear, $semester);
|
||
$name = $this->classSectionModel->getClassSectionNameBySectionId($cid);
|
||
|
||
$sections[] = [
|
||
'class_section_id' => $cid,
|
||
'class_section_name' => $name,
|
||
];
|
||
}
|
||
}
|
||
|
||
// -----------------------------
|
||
// Parents list (primaries + secondaries) for the term
|
||
// primary source: student_class->students->users
|
||
// -----------------------------
|
||
|
||
$pidQ = $this->db->table('student_class sc')
|
||
->select('DISTINCT s.parent_id', false)
|
||
->join('students s', 's.id = sc.student_id', 'left');
|
||
|
||
$pidQ->groupStart()
|
||
->whereIn('sc.school_year', $yearAliases)
|
||
->orWhere("REPLACE(REPLACE(sc.school_year,'/','-'),'–','-')", $normalizedYear)
|
||
->orWhere('sc.school_year', (string)$startYear)
|
||
->groupEnd();
|
||
|
||
$pidQ->where('s.parent_id IS NOT NULL', null, false);
|
||
|
||
$primaryIdRows = $pidQ->get()->getResultArray();
|
||
|
||
// Fallback: if no rows via student_class, try enrollments
|
||
if (empty($primaryIdRows)) {
|
||
$pidQ = $this->db->table('enrollments e')
|
||
->select('DISTINCT s.parent_id', false)
|
||
->join('students s', 's.id = e.student_id', 'left');
|
||
|
||
$pidQ->groupStart()
|
||
->whereIn('e.school_year', $yearAliases)
|
||
->orWhere("REPLACE(REPLACE(e.school_year,'/','-'),'–','-')", $normalizedYear)
|
||
->orWhere('e.school_year', (string)$startYear)
|
||
->groupEnd();
|
||
|
||
if ($limitToSemester && !empty($semAliases)) {
|
||
$pidQ->groupStart();
|
||
foreach ($semAliases as $i => $s) {
|
||
$i === 0
|
||
? $pidQ->where('LOWER(e.semester)', strtolower($s))
|
||
: $pidQ->orWhere('LOWER(e.semester)', strtolower($s));
|
||
}
|
||
$pidQ->orWhere('e.semester', $semester)->groupEnd();
|
||
} elseif ($limitToSemester) {
|
||
$pidQ->where('e.semester', $semester);
|
||
}
|
||
|
||
$pidQ->where('s.parent_id IS NOT NULL', null, false);
|
||
|
||
$primaryIdRows = $pidQ->get()->getResultArray();
|
||
}
|
||
|
||
$primaryIds = [];
|
||
foreach ($primaryIdRows as $r) {
|
||
$pid = (int)($r['parent_id'] ?? 0);
|
||
if ($pid) $primaryIds[$pid] = true;
|
||
}
|
||
$primaryIds = array_keys($primaryIds);
|
||
|
||
$primaryProfiles = [];
|
||
if (!empty($primaryIds)) {
|
||
$primRows = $this->db->table('users')
|
||
->select('id, firstname, lastname, email')
|
||
->whereIn('id', $primaryIds)
|
||
->get()->getResultArray();
|
||
|
||
foreach ($primRows as $u) {
|
||
$uid = (int)($u['id'] ?? 0);
|
||
if (!$uid) continue;
|
||
$primaryProfiles[$uid] = [
|
||
'id' => $uid,
|
||
'firstname' => (string)($u['firstname'] ?? ''),
|
||
'lastname' => (string)($u['lastname'] ?? ''),
|
||
'email' => (string)($u['email'] ?? ''),
|
||
'source' => 'users',
|
||
];
|
||
}
|
||
}
|
||
|
||
// Second parents (allow rows without semester; we’re already term-bound by primaryIds)
|
||
$secondaryProfiles = [];
|
||
if (!empty($primaryIds)) {
|
||
$secRows = $this->db->table('parents')
|
||
->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone')
|
||
->whereIn('firstparent_id', $primaryIds)
|
||
->orderBy('updated_at', 'DESC') // in case of duplicates, latest wins
|
||
->get()->getResultArray();
|
||
|
||
foreach ($secRows as $r) {
|
||
$primaryId = (int) $r['firstparent_id'];
|
||
$secondaryProfiles[$primaryId] = [
|
||
'id' => (int) $r['id'],
|
||
'firstname' => (string) $r['secondparent_firstname'],
|
||
'lastname' => (string) $r['secondparent_lastname'],
|
||
'email' => (string) $r['secondparent_email'],
|
||
'phone' => (string) $r['secondparent_phone'],
|
||
'source' => 'parents',
|
||
];
|
||
}
|
||
}
|
||
|
||
|
||
// Merge + sort
|
||
$parents = array_values($primaryProfiles);
|
||
foreach ($secondaryProfiles as $sp) $parents[] = $sp;
|
||
|
||
usort($parents, function ($a, $b) {
|
||
$ln = strcasecmp($a['lastname'] ?? '', $b['lastname'] ?? '');
|
||
if ($ln !== 0) return $ln;
|
||
$fn = strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? '');
|
||
if ($fn !== 0) return $fn;
|
||
$sa = ($a['source'] ?? '') === 'users' ? 0 : 1;
|
||
$sb = ($b['source'] ?? '') === 'users' ? 0 : 1;
|
||
return $sa <=> $sb;
|
||
});
|
||
|
||
return [$parents, $sections];
|
||
}
|
||
|
||
/** School-year aliases like 2025-2026, 2025/2026, 2025–2026, 2025-26, 2025 */
|
||
private function syAliases(string $schoolYear): array
|
||
{
|
||
$trim = trim($schoolYear);
|
||
if (preg_match('/\b(20\d{2})\b/', $trim, $m)) {
|
||
$start = (int)$m[1];
|
||
} else {
|
||
$start = (int)date('Y');
|
||
}
|
||
$end = $start + 1;
|
||
|
||
$normalized = "{$start}-{$end}";
|
||
$aliases = [
|
||
$normalized,
|
||
"{$start}/{$end}",
|
||
"{$start}–{$end}", // en dash
|
||
sprintf('%d-%02d', $start, $end % 100), // 2025-26
|
||
(string)$start, // some schemas store only start year
|
||
];
|
||
return [array_values(array_unique($aliases)), $normalized, $start];
|
||
}
|
||
|
||
/** Semester aliases */
|
||
private function semAliases(string $semester): array
|
||
{
|
||
$s = strtolower(trim($semester));
|
||
if ($s === 'fall') return ['fall', 'sem1', 'semester 1', '1', 'f'];
|
||
if ($s === 'spring') return ['spring', 'sem2', 'semester 2', '2', 's'];
|
||
if ($s === 'summer') return ['summer', 'sem3', '3'];
|
||
return [$s];
|
||
}
|
||
|
||
/**
|
||
* Returns true if student_class has any rows for the given school year.
|
||
* The semester argument is ignored because assignments are tracked per year.
|
||
*/
|
||
private function hasRosterForTerm(string $schoolYear, string $semester): bool
|
||
{
|
||
$year = trim((string)$schoolYear);
|
||
if ($year === '') {
|
||
return false;
|
||
}
|
||
|
||
if (array_key_exists($year, $this->rosterPresenceCache)) {
|
||
return $this->rosterPresenceCache[$year];
|
||
}
|
||
|
||
$cnt = $this->db->table('student_class')
|
||
->where('school_year', $year)
|
||
->countAllResults();
|
||
|
||
$this->rosterPresenceCache[$year] = $cnt > 0;
|
||
return $this->rosterPresenceCache[$year];
|
||
}
|
||
|
||
|
||
/**
|
||
* Parents mode:
|
||
* - pickedId can be a primary (users.id) or a secondary (parents.id)
|
||
* - resolve to primary -> students -> class_section_ids (from student_class)
|
||
* - collect non-empty links for those sections
|
||
* - return one bundle (emails = primary + second-parent if present)
|
||
*/
|
||
private function bundleForSingleParent(int $pickedId, array $linkBySection): ?array
|
||
{
|
||
// Determine if pickedId is primary
|
||
$isPrimary = (bool) $this->db->table('users')->select('id')->where('id', $pickedId)->get()->getFirstRow();
|
||
|
||
$primaryId = $pickedId;
|
||
if (!$isPrimary) {
|
||
// pickedId is likely a second-parent; map to firstparent_id
|
||
$map = $this->db->table('parents')
|
||
->select('firstparent_id, id')
|
||
->where('id', $pickedId)
|
||
->get()->getFirstRow('array');
|
||
if (!$map || empty($map['firstparent_id'])) {
|
||
return null;
|
||
}
|
||
$primaryId = (int) $map['firstparent_id'];
|
||
}
|
||
|
||
// Primary profile
|
||
$p = $this->db->table('users')->select('id, firstname, lastname, email')->where('id', $primaryId)->get()->getFirstRow('array');
|
||
if (!$p) return null;
|
||
|
||
// Optional second-parent profile
|
||
$sec = $this->db->table('parents')
|
||
->select('id, secondparent_firstname, secondparent_lastname, secondparent_email')
|
||
->where('firstparent_id', $primaryId)
|
||
->where('id >', 0)
|
||
->get()->getFirstRow('array');
|
||
|
||
// Sections for this primary via their students (term-scoped)
|
||
$limitToSemester = $this->hasRosterForTerm($this->schoolYear, $this->semester);
|
||
$secRows = $this->db->table('student_class sc')
|
||
->select('DISTINCT sc.class_section_id', false)
|
||
->join('students s', 's.id = sc.student_id', 'left')
|
||
->where('sc.school_year', $this->schoolYear);
|
||
$secRows = $secRows->where('s.parent_id', $primaryId)
|
||
->get()->getResultArray();
|
||
|
||
$sections = [];
|
||
$linksSet = [];
|
||
|
||
foreach ($secRows as $r) {
|
||
$cid = (int) ($r['class_section_id'] ?? 0);
|
||
if (!$cid) continue;
|
||
|
||
$link = (string) ($linkBySection[$cid]['invite_link'] ?? '');
|
||
if ($link === '') continue;
|
||
|
||
$sections[] = [
|
||
'class_section_id' => $cid,
|
||
'class_section_name' => (string) ($linkBySection[$cid]['class_section_name'] ?? ('Section ' . $cid)),
|
||
'invite_link' => $link,
|
||
];
|
||
$linksSet[$link] = true;
|
||
}
|
||
|
||
if (empty($sections)) return null;
|
||
|
||
$emailsSet = [];
|
||
if (!empty($p['email'])) $emailsSet[$p['email']] = true;
|
||
if (!empty($sec['secondparent_email'])) $emailsSet[$sec['secondparent_email']] = true;
|
||
|
||
if (empty($emailsSet)) return null;
|
||
|
||
return [
|
||
'parent' => [
|
||
'id' => (int) $p['id'],
|
||
'firstname' => (string) ($p['firstname'] ?? ''),
|
||
'lastname' => (string) ($p['lastname'] ?? ''),
|
||
'email' => (string) ($p['email'] ?? ''),
|
||
'source' => 'users',
|
||
],
|
||
'secondary_parents' => $sec && !empty($sec['id']) ? [[
|
||
'id' => (int) $sec['id'],
|
||
'firstname' => (string) ($sec['secondparent_firstname'] ?? ''),
|
||
'lastname' => (string) ($sec['secondparent_lastname'] ?? ''),
|
||
'email' => (string) ($sec['secondparent_email'] ?? ''),
|
||
'source' => 'parents',
|
||
]] : [],
|
||
'sections' => $sections,
|
||
'links' => array_keys($linksSet),
|
||
'emails' => array_keys($emailsSet),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Class mode:
|
||
* - Given classSectionId, find students (student_class) for the term
|
||
* - Map to primaries (users) and second-parents (parents)
|
||
* - Return one bundle **per primary parent** (so each parent gets their own email)
|
||
*/
|
||
private function bundleForSingleClass(int $classSectionId, array $linkBySection): array
|
||
{
|
||
$link = (string) ($linkBySection[$classSectionId]['invite_link'] ?? '');
|
||
if ($link === '') return []; // no link => skip
|
||
|
||
// Students in this class
|
||
$stuRows = $this->db->table('student_class')
|
||
->select('student_id')
|
||
->where('school_year', $this->schoolYear)
|
||
->where('semester', $this->semester)
|
||
->where('class_section_id', $classSectionId)
|
||
->get()->getResultArray();
|
||
if (empty($stuRows)) return [];
|
||
|
||
$studentIds = array_values(array_unique(array_map(fn($r) => (int) $r['student_id'], $stuRows)));
|
||
|
||
// Map students -> primary parent id
|
||
$pRows = $this->db->table('students')
|
||
->select('id, parent_id')
|
||
->whereIn('id', $studentIds)
|
||
->get()->getResultArray();
|
||
|
||
$parentIds = [];
|
||
foreach ($pRows as $r) {
|
||
$pid = (int) ($r['parent_id'] ?? 0);
|
||
if ($pid) $parentIds[$pid] = true;
|
||
}
|
||
$parentIds = array_keys($parentIds);
|
||
if (empty($parentIds)) return [];
|
||
|
||
// Load primary parent profiles
|
||
$primRows = $this->db->table('users')
|
||
->select('id, firstname, lastname, email')
|
||
->whereIn('id', $parentIds)
|
||
->get()->getResultArray();
|
||
|
||
$profiles = [];
|
||
foreach ($primRows as $u) {
|
||
$uid = (int) $u['id'];
|
||
$profiles[$uid] = [
|
||
'id' => $uid,
|
||
'firstname' => (string) ($u['firstname'] ?? ''),
|
||
'lastname' => (string) ($u['lastname'] ?? ''),
|
||
'email' => (string) ($u['email'] ?? ''),
|
||
'source' => 'users',
|
||
];
|
||
}
|
||
|
||
// Second-parents for these primaries
|
||
$secRows = $this->db->table('parents')
|
||
->select('firstparent_id, id, secondparent_firstname, secondparent_lastname, secondparent_email')
|
||
->where('id >', 0)
|
||
->whereIn('firstparent_id', $parentIds)
|
||
->get()->getResultArray();
|
||
|
||
$secByPrimary = [];
|
||
foreach ($secRows as $r) {
|
||
$fp = (int) $r['firstparent_id'];
|
||
$secByPrimary[$fp] = [
|
||
'id' => (int) ($r['id'] ?? 0),
|
||
'firstname' => (string) ($r['secondparent_firstname'] ?? ''),
|
||
'lastname' => (string) ($r['secondparent_lastname'] ?? ''),
|
||
'email' => (string) ($r['secondparent_email'] ?? ''),
|
||
'source' => 'parents',
|
||
];
|
||
}
|
||
|
||
// One bundle per primary parent
|
||
$bundles = [];
|
||
foreach ($parentIds as $pid) {
|
||
$profile = $profiles[$pid] ?? null;
|
||
if (!$profile) continue;
|
||
|
||
$emails = [];
|
||
if (!empty($profile['email'])) $emails[$profile['email']] = true;
|
||
if (!empty($secByPrimary[$pid]['email'])) $emails[$secByPrimary[$pid]['email']] = true;
|
||
if (empty($emails)) continue;
|
||
|
||
$bundles[] = [
|
||
'parent' => $profile,
|
||
'secondary_parents' => !empty($secByPrimary[$pid]['id']) ? [$secByPrimary[$pid]] : [],
|
||
'sections' => [[
|
||
'class_section_id' => $classSectionId,
|
||
'class_section_name' => (string) ($linkBySection[$classSectionId]['class_section_name'] ?? ('Section ' . $classSectionId)),
|
||
'invite_link' => $link,
|
||
]],
|
||
'links' => [$link],
|
||
'emails' => array_keys($emails),
|
||
];
|
||
}
|
||
|
||
return $bundles;
|
||
}
|
||
|
||
/**
|
||
* All mode:
|
||
* - Iterate all distinct class_section_id in student_class (for the term)
|
||
* - Reuse class bundles per section and flatten
|
||
*/
|
||
private function bundlesForAllParentsAllClasses(array $linkBySection): array
|
||
{
|
||
$secRows = $this->db->table('student_class')
|
||
->select('DISTINCT class_section_id', false)
|
||
->where('school_year', $this->schoolYear)
|
||
->where('semester', $this->semester)
|
||
->orderBy('class_section_id', 'ASC')
|
||
->get()->getResultArray();
|
||
|
||
$bundles = [];
|
||
foreach ($secRows as $r) {
|
||
$cid = (int) ($r['class_section_id'] ?? 0);
|
||
if (!$cid) continue;
|
||
|
||
$classBundles = $this->bundleForSingleClass($cid, $linkBySection);
|
||
foreach ($classBundles as $b) {
|
||
$bundles[] = $b;
|
||
}
|
||
}
|
||
return $bundles;
|
||
}
|
||
}
|