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; } }