1181 lines
45 KiB
PHP
Executable File
1181 lines
45 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\Http\Controllers\Api;
|
||
|
||
use App\Models\Configuration;
|
||
use App\Models\WhatsappGroupLink;
|
||
use App\Models\WhatsappGroupMembership;
|
||
use App\Models\ClassSection;
|
||
use App\Models\Student;
|
||
use App\Models\User;
|
||
use App\Models\Enrollment;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Event;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
|
||
class WhatsappController extends BaseApiController
|
||
{
|
||
protected WhatsappGroupLink $link;
|
||
protected WhatsappGroupMembership $membership;
|
||
protected Configuration $config;
|
||
protected ClassSection $classSection;
|
||
protected Student $student;
|
||
protected User $user;
|
||
protected Enrollment $enrollment;
|
||
protected string $schoolYear;
|
||
protected string $semester;
|
||
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
$this->link = model(WhatsappGroupLink::class);
|
||
$this->membership = model(WhatsappGroupMembership::class);
|
||
$this->config = model(Configuration::class);
|
||
$this->classSection = model(ClassSection::class);
|
||
$this->student = model(Student::class);
|
||
$this->user = model(User::class);
|
||
$this->enrollment = model(Enrollment::class);
|
||
$this->schoolYear = (string) ($this->config->getConfig('school_year') ?? '');
|
||
$this->semester = (string) ($this->config->getConfig('semester') ?? '');
|
||
}
|
||
|
||
public function links()
|
||
{
|
||
try {
|
||
$links = $this->link->newQuery()
|
||
->where('school_year', $this->schoolYear)
|
||
->where('semester', $this->semester)
|
||
->orderBy('class_section_name', 'ASC')
|
||
->get()
|
||
->getResultArray();
|
||
|
||
return $this->success($links, 'WhatsApp links retrieved successfully');
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'WhatsApp links error: ' . $e->getMessage());
|
||
return $this->respondError('Failed to retrieve WhatsApp links', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||
}
|
||
}
|
||
|
||
public function saveLink()
|
||
{
|
||
$payload = $this->payloadData();
|
||
if (empty($payload)) {
|
||
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
$rules = [
|
||
'class_section_id' => 'required|integer',
|
||
'invite_link' => 'required|string',
|
||
'active' => 'nullable|in:0,1',
|
||
];
|
||
|
||
$errors = $this->validateRequest($payload, $rules);
|
||
if (!empty($errors)) {
|
||
return $this->respondValidationError($errors);
|
||
}
|
||
|
||
$sectionId = (int) $payload['class_section_id'];
|
||
$sectionName = trim((string) ($payload['class_section_name'] ?? ''));
|
||
$inviteLink = trim((string) $payload['invite_link']);
|
||
$active = isset($payload['active']) ? (int) $payload['active'] : 1;
|
||
|
||
if ($inviteLink === '') {
|
||
return $this->error('Invite link is required', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||
}
|
||
|
||
try {
|
||
$existing = $this->link->where([
|
||
'class_section_id' => $sectionId,
|
||
'school_year' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
])->first();
|
||
|
||
// Resolve display name if not provided
|
||
if ($sectionName === '') {
|
||
$row = $this->link->getLinkForSection($sectionId, $this->schoolYear, $this->semester);
|
||
$sectionName = $row['class_section_name'] ?? '';
|
||
if ($sectionName === '') {
|
||
$sectionName = $this->classSection->getClassSectionNameBySectionId($sectionId) ?? ('Section ' . $sectionId);
|
||
}
|
||
}
|
||
|
||
$linkData = [
|
||
'class_section_id' => $sectionId,
|
||
'class_section_name' => $sectionName,
|
||
'invite_link' => $inviteLink,
|
||
'active' => $active ? 1 : 0,
|
||
'school_year' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
];
|
||
|
||
if ($existing) {
|
||
$linkId = (int) $existing['id'];
|
||
$this->link->update($linkId, $linkData);
|
||
} else {
|
||
$linkId = (int) $this->link->insert($linkData);
|
||
}
|
||
|
||
if (!$linkId) {
|
||
return $this->respondError('Failed to save WhatsApp link', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||
}
|
||
|
||
$link = $this->link->find($linkId);
|
||
return $this->success($link, 'WhatsApp link saved successfully');
|
||
} catch (\Throwable $e) {
|
||
log_message('error', 'WhatsApp link save error: ' . $e->getMessage());
|
||
return $this->respondError('Failed to save WhatsApp link', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||
}
|
||
}
|
||
|
||
public function updateMembership()
|
||
{
|
||
$payload = $this->payloadData();
|
||
if (empty($payload)) {
|
||
return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
$sid = (int) ($payload['class_section_id'] ?? 0);
|
||
$primaryId = (int) ($payload['primary_id'] ?? 0);
|
||
$secondId = (int) ($payload['second_id'] ?? 0);
|
||
|
||
$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 = $payload['primary_member'] ?? null;
|
||
$secondValueRaw = $payload['second_member'] ?? null;
|
||
|
||
if ($sid <= 0) {
|
||
return $this->respondError('Invalid class section.', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
$schoolYear = trim((string) ($payload['school_year'] ?? $this->schoolYear ?? ''));
|
||
$semester = trim((string) ($payload['semester'] ?? $this->semester ?? ''));
|
||
|
||
$verifiedBy = $this->getCurrentUserId();
|
||
|
||
try {
|
||
$saved = [];
|
||
|
||
if ($primaryId > 0) {
|
||
$isMember = $isChecked($primaryValueRaw);
|
||
$id = $this->membership->upsertMembership($sid, $schoolYear, $semester, 'primary', $primaryId, $isMember, $verifiedBy);
|
||
if ($id > 0) {
|
||
$saved[] = 'primary=' . $primaryId . ':' . ($isMember ? '1' : '0');
|
||
}
|
||
}
|
||
|
||
if ($secondId > 0) {
|
||
$isMember = $isChecked($secondValueRaw);
|
||
$id = $this->membership->upsertMembership($sid, $schoolYear, $semester, 'second', $secondId, $isMember, $verifiedBy);
|
||
if ($id > 0) {
|
||
$saved[] = 'second=' . $secondId . ':' . ($isMember ? '1' : '0');
|
||
}
|
||
}
|
||
|
||
if (empty($saved)) {
|
||
return $this->respondError('Nothing to save (missing parent IDs).', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
return $this->success([
|
||
'saved' => $saved,
|
||
'term' => ['school_year' => $schoolYear, 'semester' => $semester],
|
||
], 'Membership updated successfully');
|
||
} catch (\Throwable $e) {
|
||
Log::error('Failed to update WhatsApp membership: ' . $e->getMessage());
|
||
return $this->respondError('Failed to save membership.', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/whatsapp/index
|
||
* Get initial data for WhatsApp management page
|
||
*/
|
||
public function index()
|
||
{
|
||
[$parents, $sections] = $this->prepareIndexData();
|
||
|
||
// Build links map
|
||
$links = $this->link->getAllForTerm($this->schoolYear, $this->semester);
|
||
$linksBySection = [];
|
||
foreach ($links as $l) {
|
||
$linksBySection[(int) $l['class_section_id']] = $l;
|
||
}
|
||
|
||
return $this->success([
|
||
'sections' => $sections,
|
||
'parents' => $parents,
|
||
'linksBySection' => $linksBySection,
|
||
'schoolYear' => $this->schoolYear,
|
||
'semester' => $this->semester,
|
||
], 'WhatsApp management data retrieved successfully');
|
||
}
|
||
|
||
/**
|
||
* POST /api/v1/whatsapp/send-invites
|
||
* Send invites in three modes: 'parents' | 'class' | 'all'
|
||
*/
|
||
public function sendInvites()
|
||
{
|
||
$data = $this->payloadData();
|
||
$mode = trim((string) ($data['mode'] ?? ''));
|
||
$parentIdsPicked = array_map('intval', (array) ($data['parent_ids'] ?? []));
|
||
$rawClassId = $data['class_section_id'] ?? null;
|
||
$classSectionId = ($rawClassId === '' || $rawClassId === null) ? 0 : (int) $rawClassId;
|
||
|
||
if (!in_array($mode, ['parents', 'class', 'all'], true)) {
|
||
return $this->respondError('Invalid mode.', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
if ($mode === 'parents' && empty(array_filter($parentIdsPicked))) {
|
||
return $this->respondError('Please select at least one parent.', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
if ($mode === 'class' && $classSectionId <= 0) {
|
||
return $this->respondError('Please select a class/section.', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
// Load all links for the term once
|
||
$links = $this->link->getAllForTerm($this->schoolYear, $this->semester);
|
||
$linkBySection = [];
|
||
foreach ($links as $l) {
|
||
$cid = (int) ($l['class_section_id'] ?? 0);
|
||
if ($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);
|
||
foreach ($bundles as $b) {
|
||
if (!empty($b['emails']) && !empty($b['links'])) {
|
||
$payloads[] = $b;
|
||
}
|
||
}
|
||
} else { // 'all'
|
||
$bundles = $this->bundlesForAllParentsAllClasses($linkBySection);
|
||
foreach ($bundles as $b) {
|
||
if (!empty($b['emails']) && !empty($b['links'])) {
|
||
$payloads[] = $b;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($payloads)) {
|
||
return $this->respondError('No recipients found (missing links or emails).', Response::HTTP_BAD_REQUEST);
|
||
}
|
||
|
||
// Consolidate per-student bundles into one bundle per parent
|
||
$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 {
|
||
$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);
|
||
};
|
||
|
||
$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 {
|
||
$byKey['name:' . (string) $row] = $row;
|
||
}
|
||
};
|
||
foreach ($A as $r) $put($byKey, $r);
|
||
foreach ($B as $r) $put($byKey, $r);
|
||
return array_values($byKey);
|
||
};
|
||
|
||
$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'] ?? []),
|
||
'items' => (array) ($b['items'] ?? []),
|
||
];
|
||
} else {
|
||
$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'] ?? []));
|
||
}
|
||
}
|
||
|
||
$payloads = array_values($grouped);
|
||
|
||
$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);
|
||
};
|
||
|
||
$getSecondaryEmails = function (array $bundle) use ($norm) {
|
||
$collected = [];
|
||
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'];
|
||
}
|
||
}
|
||
$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'];
|
||
|
||
try {
|
||
$primaryId = (int) ($p['id'] ?? 0);
|
||
if ($primaryId > 0) {
|
||
$row = DB::table('parents')
|
||
->select('secondparent_email')
|
||
->where('firstparent_id', $primaryId)
|
||
->orderBy('updated_at', 'DESC')
|
||
->first();
|
||
if ($row && !empty($row->secondparent_email)) {
|
||
$collected[] = $row->secondparent_email;
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::error('Secondary email DB lookup failed: ' . $e->getMessage());
|
||
}
|
||
|
||
return $norm($collected);
|
||
};
|
||
|
||
// Trigger one event per family
|
||
$triggered = 0;
|
||
foreach ($payloads as $b) {
|
||
try {
|
||
$primaryEmails = $norm((array) ($b['emails'] ?? []));
|
||
$secondaryEmails = $getSecondaryEmails($b);
|
||
|
||
$to = $primaryEmails;
|
||
$cc = [];
|
||
|
||
if (!empty($primaryEmails) && !empty($secondaryEmails)) {
|
||
$cc = array_values(array_diff($secondaryEmails, $primaryEmails));
|
||
} elseif (empty($primaryEmails) && !empty($secondaryEmails)) {
|
||
$to = $secondaryEmails;
|
||
}
|
||
|
||
if (empty($to)) {
|
||
Log::warning('Skipping whatsapp_invites.send: no recipient email(s) for parent_id ' . ($b['parent']['id'] ?? null));
|
||
continue;
|
||
}
|
||
|
||
Event::dispatch('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::error('whatsapp_invites.send failed: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
return $this->success([
|
||
'triggered' => $triggered,
|
||
'total' => count($payloads),
|
||
], "Invite processing started for {$triggered} recipient group(s).");
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/whatsapp/parent-contacts
|
||
* Get parent contacts list
|
||
*/
|
||
public function parentContacts()
|
||
{
|
||
$schoolYear = $this->schoolYear ?? (string) ($this->request->getGet('school_year') ?? '');
|
||
$semester = $this->semester ?? (string) ($this->request->getGet('semester') ?? '');
|
||
|
||
$b = DB::table('parents sp')
|
||
->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',
|
||
])
|
||
->leftJoin('users u', 'u.id', '=', 'sp.firstparent_id');
|
||
|
||
if ($schoolYear !== '') {
|
||
$b->where('sp.school_year', $schoolYear);
|
||
}
|
||
if ($semester !== '') {
|
||
$b->where('sp.semester', $semester);
|
||
}
|
||
|
||
$rows = $b->get();
|
||
|
||
$contacts = [];
|
||
foreach ($rows as $r) {
|
||
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',
|
||
];
|
||
}
|
||
|
||
$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',
|
||
];
|
||
}
|
||
}
|
||
|
||
$contacts = array_values(array_filter($contacts, function ($c) {
|
||
$name = trim(($c['firstname'] ?? '') . ($c['lastname'] ?? ''));
|
||
$contact = trim(($c['phone'] ?? '') . ($c['email'] ?? ''));
|
||
return $name !== '' || $contact !== '';
|
||
}));
|
||
|
||
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;
|
||
$ta = ($a['type'] ?? '') === 'Primary Parent' ? 0 : 1;
|
||
$tb = ($b['type'] ?? '') === 'Primary Parent' ? 0 : 1;
|
||
return $ta <=> $tb;
|
||
});
|
||
|
||
return $this->success([
|
||
'contacts' => $contacts,
|
||
'schoolYear' => $schoolYear,
|
||
'semester' => $semester,
|
||
], 'Parent contacts retrieved successfully');
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/whatsapp/parent-contacts-by-class
|
||
* Get parent contacts grouped by class section
|
||
*/
|
||
public function parentContactsByClass()
|
||
{
|
||
$schoolYear = $this->schoolYear ?? (string) ($this->request->getGet('school_year') ?? '');
|
||
$semester = $this->semester ?? (string) ($this->request->getGet('semester') ?? '');
|
||
|
||
// Get distinct class_section_id from student_class
|
||
$csb = DB::table('student_class sc')
|
||
->select('sc.class_section_id', DB::raw('MAX(sc.semester) as semester'), DB::raw('MAX(sc.school_year) as school_year'));
|
||
|
||
if ($schoolYear !== '') $csb->where('sc.school_year', $schoolYear);
|
||
if ($semester !== '') $csb->where('sc.semester', $semester);
|
||
|
||
$sections = $csb->groupBy('sc.class_section_id')
|
||
->orderBy('sc.class_section_id', 'ASC')
|
||
->get();
|
||
|
||
$sectionIds = array_values(array_filter(array_unique($sections->pluck('class_section_id')->map(fn($id) => (int) $id)->toArray())));
|
||
|
||
// Fetch names from classSection
|
||
$namesById = [];
|
||
if (!empty($sectionIds)) {
|
||
$nb = DB::table('classSection')
|
||
->select('class_section_id', 'class_section_name')
|
||
->whereIn('class_section_id', $sectionIds)
|
||
->get();
|
||
|
||
foreach ($nb as $row) {
|
||
$namesById[(int) $row->class_section_id] = (string) ($row->class_section_name ?? '');
|
||
}
|
||
}
|
||
|
||
$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' => (string) ($s->semester ?? ''),
|
||
'school_year' => (string) ($s->school_year ?? ''),
|
||
'description' => '',
|
||
'main_teachers' => [],
|
||
'teacher_assistants' => [],
|
||
'parents' => [],
|
||
];
|
||
}
|
||
|
||
// Parents per class
|
||
$pb = DB::table('student_class sc')
|
||
->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',
|
||
])
|
||
->join('students s', 's.id', '=', 'sc.student_id')
|
||
->join('users u', 'u.id', '=', 's.parent_id')
|
||
->leftJoin('parents sp', function ($join) {
|
||
$join->on('sp.firstparent_id', '=', 'u.id')
|
||
->whereColumn('sp.school_year', 'sc.school_year')
|
||
->where(function ($q) {
|
||
$q->whereColumn('sp.semester', 'sc.semester')
|
||
->orWhereNull('sp.semester')
|
||
->orWhere('sp.semester', '');
|
||
});
|
||
});
|
||
|
||
if ($schoolYear !== '') $pb->where('sc.school_year', $schoolYear);
|
||
if ($semester !== '') $pb->where('sc.semester', $semester);
|
||
|
||
$rows = $pb->orderBy('sc.class_section_id', 'ASC')
|
||
->orderBy('u.lastname', 'ASC')
|
||
->orderBy('u.firstname', 'ASC')
|
||
->get();
|
||
|
||
$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 ?? ''),
|
||
'primary_member' => null,
|
||
'second_member' => null,
|
||
];
|
||
}
|
||
|
||
// Annotate membership flags
|
||
try {
|
||
$sectionIds = array_keys($classSections);
|
||
$m = $this->membership->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
|
||
}
|
||
|
||
// Teachers
|
||
try {
|
||
$tb = DB::table('teacher_class tc')
|
||
->select('tc.class_section_id', 'tc.position', 'u.firstname', 'u.lastname')
|
||
->leftJoin('users u', 'u.id', '=', 'tc.teacher_id');
|
||
|
||
if ($schoolYear !== '') $tb->where('tc.school_year', $schoolYear);
|
||
if ($semester !== '') $tb->where('tc.semester', $semester);
|
||
|
||
$teachers = $tb->get();
|
||
|
||
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 $this->success([
|
||
'classSections' => array_values($classSections),
|
||
], 'Parent contacts by class retrieved successfully');
|
||
}
|
||
|
||
/**
|
||
* Build initial lists for the page
|
||
*/
|
||
private function prepareIndexData(): array
|
||
{
|
||
[$yearAliases, $normalizedYear, $startYear] = $this->syAliases($this->schoolYear);
|
||
$semAliases = $this->semAliases($this->semester);
|
||
|
||
// Sections list
|
||
$secQ = DB::table('student_class sc')
|
||
->select(DB::raw('DISTINCT sc.class_section_id'));
|
||
|
||
$secQ->where(function ($q) use ($yearAliases, $normalizedYear, $startYear) {
|
||
$q->whereIn('sc.school_year', $yearAliases)
|
||
->orWhereRaw("REPLACE(REPLACE(sc.school_year,'/','-'),'–','-') = ?", [$normalizedYear])
|
||
->orWhere('sc.school_year', (string) $startYear);
|
||
});
|
||
|
||
if (!empty($semAliases)) {
|
||
$secQ->where(function ($q) use ($semAliases) {
|
||
foreach ($semAliases as $i => $s) {
|
||
if ($i === 0) {
|
||
$q->whereRaw('LOWER(sc.semester) = ?', [strtolower($s)]);
|
||
} else {
|
||
$q->orWhereRaw('LOWER(sc.semester) = ?', [strtolower($s)]);
|
||
}
|
||
}
|
||
$q->orWhere('sc.semester', $this->semester);
|
||
});
|
||
} else {
|
||
$secQ->where('sc.semester', $this->semester);
|
||
}
|
||
|
||
$secRows = $secQ->orderBy('sc.class_section_id', 'ASC')->get();
|
||
|
||
// Fallback to enrollments
|
||
if ($secRows->isEmpty()) {
|
||
$secQ = DB::table('enrollments e')
|
||
->select(DB::raw('DISTINCT e.class_section_id'));
|
||
|
||
$secQ->where(function ($q) use ($yearAliases, $normalizedYear, $startYear) {
|
||
$q->whereIn('e.school_year', $yearAliases)
|
||
->orWhereRaw("REPLACE(REPLACE(e.school_year,'/','-'),'–','-') = ?", [$normalizedYear])
|
||
->orWhere('e.school_year', (string) $startYear);
|
||
});
|
||
|
||
if (!empty($semAliases)) {
|
||
$secQ->where(function ($q) use ($semAliases) {
|
||
foreach ($semAliases as $i => $s) {
|
||
if ($i === 0) {
|
||
$q->whereRaw('LOWER(e.semester) = ?', [strtolower($s)]);
|
||
} else {
|
||
$q->orWhereRaw('LOWER(e.semester) = ?', [strtolower($s)]);
|
||
}
|
||
}
|
||
$q->orWhere('e.semester', $this->semester);
|
||
});
|
||
} else {
|
||
$secQ->where('e.semester', $this->semester);
|
||
}
|
||
|
||
$secRows = $secQ->orderBy('e.class_section_id', 'ASC')->get();
|
||
}
|
||
|
||
$sections = [];
|
||
foreach ($secRows as $r) {
|
||
$cid = (int) ($r->class_section_id ?? 0);
|
||
if (!$cid) continue;
|
||
|
||
$linkRow = $this->link->getLinkForSection($cid, $this->schoolYear, $this->semester);
|
||
$name = $this->classSection->getClassSectionNameBySectionId($cid) ?? ('Section ' . $cid);
|
||
|
||
$sections[] = [
|
||
'class_section_id' => $cid,
|
||
'class_section_name' => $name,
|
||
];
|
||
}
|
||
|
||
// Parents list
|
||
$pidQ = DB::table('student_class sc')
|
||
->select(DB::raw('DISTINCT s.parent_id'))
|
||
->leftJoin('students s', 's.id', '=', 'sc.student_id');
|
||
|
||
$pidQ->where(function ($q) use ($yearAliases, $normalizedYear, $startYear) {
|
||
$q->whereIn('sc.school_year', $yearAliases)
|
||
->orWhereRaw("REPLACE(REPLACE(sc.school_year,'/','-'),'–','-') = ?", [$normalizedYear])
|
||
->orWhere('sc.school_year', (string) $startYear);
|
||
});
|
||
|
||
if (!empty($semAliases)) {
|
||
$pidQ->where(function ($q) use ($semAliases) {
|
||
foreach ($semAliases as $i => $s) {
|
||
if ($i === 0) {
|
||
$q->whereRaw('LOWER(sc.semester) = ?', [strtolower($s)]);
|
||
} else {
|
||
$q->orWhereRaw('LOWER(sc.semester) = ?', [strtolower($s)]);
|
||
}
|
||
}
|
||
$q->orWhere('sc.semester', $this->semester);
|
||
});
|
||
} else {
|
||
$pidQ->where('sc.semester', $this->semester);
|
||
}
|
||
|
||
$pidQ->whereNotNull('s.parent_id');
|
||
|
||
$primaryIdRows = $pidQ->get();
|
||
|
||
// Fallback to enrollments
|
||
if ($primaryIdRows->isEmpty()) {
|
||
$pidQ = DB::table('enrollments e')
|
||
->select(DB::raw('DISTINCT s.parent_id'))
|
||
->leftJoin('students s', 's.id', '=', 'e.student_id');
|
||
|
||
$pidQ->where(function ($q) use ($yearAliases, $normalizedYear, $startYear) {
|
||
$q->whereIn('e.school_year', $yearAliases)
|
||
->orWhereRaw("REPLACE(REPLACE(e.school_year,'/','-'),'–','-') = ?", [$normalizedYear])
|
||
->orWhere('e.school_year', (string) $startYear);
|
||
});
|
||
|
||
if (!empty($semAliases)) {
|
||
$pidQ->where(function ($q) use ($semAliases) {
|
||
foreach ($semAliases as $i => $s) {
|
||
if ($i === 0) {
|
||
$q->whereRaw('LOWER(e.semester) = ?', [strtolower($s)]);
|
||
} else {
|
||
$q->orWhereRaw('LOWER(e.semester) = ?', [strtolower($s)]);
|
||
}
|
||
}
|
||
$q->orWhere('e.semester', $this->semester);
|
||
});
|
||
} else {
|
||
$pidQ->where('e.semester', $this->semester);
|
||
}
|
||
|
||
$pidQ->whereNotNull('s.parent_id');
|
||
$primaryIdRows = $pidQ->get();
|
||
}
|
||
|
||
$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 = DB::table('users')
|
||
->select('id', 'firstname', 'lastname', 'email')
|
||
->whereIn('id', $primaryIds)
|
||
->get();
|
||
|
||
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
|
||
$secondaryProfiles = [];
|
||
if (!empty($primaryIds)) {
|
||
$secRows = DB::table('parents')
|
||
->select('id', 'firstparent_id', 'secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone')
|
||
->whereIn('firstparent_id', $primaryIds)
|
||
->orderBy('updated_at', 'DESC')
|
||
->get();
|
||
|
||
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
|
||
*/
|
||
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}",
|
||
sprintf('%d-%02d', $start, $end % 100),
|
||
(string) $start,
|
||
];
|
||
|
||
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];
|
||
}
|
||
|
||
/**
|
||
* Bundle for single parent
|
||
*/
|
||
private function bundleForSingleParent(int $pickedId, array $linkBySection): ?array
|
||
{
|
||
$isPrimary = (bool) DB::table('users')->where('id', $pickedId)->first();
|
||
$primaryId = $pickedId;
|
||
|
||
if (!$isPrimary) {
|
||
$map = DB::table('parents')
|
||
->select('firstparent_id', 'id')
|
||
->where('id', $pickedId)
|
||
->first();
|
||
|
||
if (!$map || empty($map->firstparent_id)) {
|
||
return null;
|
||
}
|
||
$primaryId = (int) $map->firstparent_id;
|
||
}
|
||
|
||
$p = DB::table('users')
|
||
->select('id', 'firstname', 'lastname', 'email')
|
||
->where('id', $primaryId)
|
||
->first();
|
||
|
||
if (!$p) return null;
|
||
|
||
$sec = DB::table('parents')
|
||
->select('id', 'secondparent_firstname', 'secondparent_lastname', 'secondparent_email')
|
||
->where('firstparent_id', $primaryId)
|
||
->where('id', '>', 0)
|
||
->first();
|
||
|
||
// Sections for this primary
|
||
$secRows = DB::table('student_class sc')
|
||
->select(DB::raw('DISTINCT sc.class_section_id'))
|
||
->leftJoin('students s', 's.id', '=', 'sc.student_id')
|
||
->where('sc.school_year', $this->schoolYear)
|
||
->where('sc.semester', $this->semester)
|
||
->where('s.parent_id', $primaryId)
|
||
->get();
|
||
|
||
$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 ($sec && !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),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Bundle for single class
|
||
*/
|
||
private function bundleForSingleClass(int $classSectionId, array $linkBySection): array
|
||
{
|
||
$link = (string) ($linkBySection[$classSectionId]['invite_link'] ?? '');
|
||
if ($link === '') return [];
|
||
|
||
// Students in this class
|
||
$stuRows = DB::table('student_class')
|
||
->select('student_id')
|
||
->where('school_year', $this->schoolYear)
|
||
->where('semester', $this->semester)
|
||
->where('class_section_id', $classSectionId)
|
||
->get();
|
||
|
||
if ($stuRows->isEmpty()) return [];
|
||
|
||
$studentIds = array_values(array_unique($stuRows->pluck('student_id')->map(fn($id) => (int) $id)->toArray()));
|
||
|
||
// Map students -> primary parent id
|
||
$pRows = DB::table('students')
|
||
->select('id', 'parent_id')
|
||
->whereIn('id', $studentIds)
|
||
->get();
|
||
|
||
$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 = DB::table('users')
|
||
->select('id', 'firstname', 'lastname', 'email')
|
||
->whereIn('id', $parentIds)
|
||
->get();
|
||
|
||
$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
|
||
$secRows = DB::table('parents')
|
||
->select('firstparent_id', 'id', 'secondparent_firstname', 'secondparent_lastname', 'secondparent_email')
|
||
->where('id', '>', 0)
|
||
->whereIn('firstparent_id', $parentIds)
|
||
->get();
|
||
|
||
$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;
|
||
}
|
||
|
||
/**
|
||
* Bundles for all parents all classes
|
||
*/
|
||
private function bundlesForAllParentsAllClasses(array $linkBySection): array
|
||
{
|
||
$secRows = DB::table('student_class')
|
||
->select(DB::raw('DISTINCT class_section_id'))
|
||
->where('school_year', $this->schoolYear)
|
||
->where('semester', $this->semester)
|
||
->orderBy('class_section_id', 'ASC')
|
||
->get();
|
||
|
||
$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;
|
||
}
|
||
}
|
||
|