Files
alrahma_sunday_school_api/app/Http/Controllers/Api/WhatsappController.php
T
2026-03-05 12:29:37 -05:00

1181 lines
45 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}