add whatsup logic
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Whatsapp;
|
||||
|
||||
use App\Models\WhatsappGroupMembership;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WhatsappContactService
|
||||
{
|
||||
public function __construct(private WhatsappContextService $context)
|
||||
{
|
||||
}
|
||||
|
||||
public function listParentContacts(?string $schoolYear, ?string $semester): array
|
||||
{
|
||||
$schoolYear = trim((string) $schoolYear);
|
||||
$semester = trim((string) $semester);
|
||||
|
||||
$builder = DB::table('parents as sp');
|
||||
$builder->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
|
||||
");
|
||||
$builder->leftJoin('users as u', 'u.id', '=', 'sp.firstparent_id');
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$builder->where('sp.school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$builder->where('sp.semester', $semester);
|
||||
}
|
||||
|
||||
$rows = $builder->get()->all();
|
||||
|
||||
$contacts = [];
|
||||
foreach ($rows as $row) {
|
||||
$r = (array) $row;
|
||||
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 $contacts;
|
||||
}
|
||||
|
||||
public function listParentContactsByClass(?string $schoolYear, ?string $semester): array
|
||||
{
|
||||
$schoolYear = trim((string) $schoolYear);
|
||||
$semester = trim((string) $semester);
|
||||
|
||||
$sections = DB::table('student_class as sc')
|
||||
->select('sc.class_section_id', DB::raw('MAX(sc.school_year) AS school_year'))
|
||||
->when($schoolYear !== '', fn ($q) => $q->where('sc.school_year', $schoolYear))
|
||||
->groupBy('sc.class_section_id')
|
||||
->orderBy('sc.class_section_id', 'ASC')
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$sectionIds = array_values(array_filter(array_unique(array_map(
|
||||
fn ($r) => (int) ($r->class_section_id ?? 0),
|
||||
$sections
|
||||
))));
|
||||
|
||||
$namesById = [];
|
||||
if (!empty($sectionIds)) {
|
||||
$nameRows = DB::table('classSection')
|
||||
->select('class_section_id', 'class_section_name')
|
||||
->whereIn('class_section_id', $sectionIds)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
foreach ($nameRows 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' => $semester,
|
||||
'school_year' => (string) ($s->school_year ?? ''),
|
||||
'description' => '',
|
||||
'main_teachers' => [],
|
||||
'teacher_assistants' => [],
|
||||
'parents' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$rows = DB::table('student_class as 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 as s', 's.id', '=', 'sc.student_id')
|
||||
->join('users as u', 'u.id', '=', 's.parent_id')
|
||||
->leftJoin('parents as sp', function ($join) {
|
||||
$join->on('sp.firstparent_id', '=', 'u.id')
|
||||
->on('sp.school_year', '=', 'sc.school_year');
|
||||
})
|
||||
->when($schoolYear !== '', fn ($q) => $q->where('sc.school_year', $schoolYear))
|
||||
->orderBy('sc.class_section_id', 'ASC')
|
||||
->orderBy('u.lastname', 'ASC')
|
||||
->orderBy('u.firstname', 'ASC')
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$seen = [];
|
||||
foreach ($rows as $row) {
|
||||
$r = (array) $row;
|
||||
$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,
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$sectionIds = array_keys($classSections);
|
||||
$membershipMap = WhatsappGroupMembership::getBySectionsAndTerm($sectionIds, $schoolYear, $semester);
|
||||
foreach ($classSections as $sid => &$section) {
|
||||
foreach ($section['parents'] as &$parent) {
|
||||
$key = sprintf('%d:primary:%d', (int) $parent['class_section_id'], (int) $parent['primary_id']);
|
||||
if (isset($membershipMap[$key])) {
|
||||
$parent['primary_member'] = (int) ($membershipMap[$key]['is_member'] ?? 0);
|
||||
}
|
||||
|
||||
$secondId = (int) ($parent['second_id'] ?? 0);
|
||||
if ($secondId > 0) {
|
||||
$key2 = sprintf('%d:second:%d', (int) $parent['class_section_id'], $secondId);
|
||||
if (isset($membershipMap[$key2])) {
|
||||
$parent['second_member'] = (int) ($membershipMap[$key2]['is_member'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($parent);
|
||||
}
|
||||
unset($section);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('WhatsApp membership lookup failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$teachers = DB::table('teacher_class as tc')
|
||||
->select('tc.class_section_id', 'tc.position', 'u.firstname', 'u.lastname')
|
||||
->leftJoin('users as u', 'u.id', '=', 'tc.teacher_id')
|
||||
->when($schoolYear !== '', fn ($q) => $q->where('tc.school_year', $schoolYear))
|
||||
->get()
|
||||
->all();
|
||||
|
||||
foreach ($teachers as $row) {
|
||||
$r = (array) $row;
|
||||
$sid = (int) ($r['class_section_id'] ?? 0);
|
||||
if (!isset($classSections[$sid])) {
|
||||
continue;
|
||||
}
|
||||
$name = trim(($r['lastname'] ?? '') . ', ' . ($r['firstname'] ?? ''));
|
||||
$pos = strtolower((string) ($r['position'] ?? 'main'));
|
||||
if ($pos === 'ta' || $pos === 'assistant') {
|
||||
$classSections[$sid]['teacher_assistants'][] = $name;
|
||||
} else {
|
||||
$classSections[$sid]['main_teachers'][] = $name;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('WhatsApp teacher list lookup failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
foreach ($classSections as &$section) {
|
||||
$section['main_teachers'] = array_values(array_filter($section['main_teachers'] ?? []));
|
||||
$section['teacher_assistants'] = array_values(array_filter($section['teacher_assistants'] ?? []));
|
||||
$section['parents'] = array_values($section['parents'] ?? []);
|
||||
}
|
||||
unset($section);
|
||||
|
||||
return array_values($classSections);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Whatsapp;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Models\StudentClass;
|
||||
|
||||
class WhatsappContextService
|
||||
{
|
||||
/** @var array<string,bool> */
|
||||
private array $rosterPresenceCache = [];
|
||||
|
||||
public function schoolYear(?string $override = null): string
|
||||
{
|
||||
$override = trim((string) $override);
|
||||
if ($override !== '') {
|
||||
return $override;
|
||||
}
|
||||
|
||||
return trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
}
|
||||
|
||||
public function semester(?string $override = null): string
|
||||
{
|
||||
$override = trim((string) $override);
|
||||
if ($override !== '') {
|
||||
return $override;
|
||||
}
|
||||
|
||||
return trim((string) (Configuration::getConfig('semester') ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* School-year aliases like 2025-2026, 2025/2026, 2025-26, 2025.
|
||||
*
|
||||
* @return array{0: array<int,string>, 1: string, 2: int}
|
||||
*/
|
||||
public function schoolYearAliases(string $schoolYear): array
|
||||
{
|
||||
$trim = trim($schoolYear);
|
||||
$normalized = str_replace(["\u{2013}", "\u{2014}", '/'], '-', $trim);
|
||||
|
||||
if (preg_match('/\b(20\d{2})\b/', $normalized, $m)) {
|
||||
$start = (int) $m[1];
|
||||
} else {
|
||||
$start = (int) date('Y');
|
||||
}
|
||||
|
||||
$end = $start + 1;
|
||||
$canonical = $start . '-' . $end;
|
||||
|
||||
$aliases = [
|
||||
$canonical,
|
||||
$start . '/' . $end,
|
||||
sprintf('%d-%02d', $start, $end % 100),
|
||||
(string) $start,
|
||||
];
|
||||
|
||||
return [array_values(array_unique($aliases)), $canonical, $start];
|
||||
}
|
||||
|
||||
/**
|
||||
* Semester aliases for filtering.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public function semesterAliases(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 === '' ? [] : [$s];
|
||||
}
|
||||
|
||||
public function hasRosterForYear(string $schoolYear): bool
|
||||
{
|
||||
$year = trim($schoolYear);
|
||||
if ($year === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (array_key_exists($year, $this->rosterPresenceCache)) {
|
||||
return $this->rosterPresenceCache[$year];
|
||||
}
|
||||
|
||||
$exists = StudentClass::query()
|
||||
->where('school_year', $year)
|
||||
->exists();
|
||||
|
||||
$this->rosterPresenceCache[$year] = $exists;
|
||||
|
||||
return $exists;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Whatsapp;
|
||||
|
||||
use App\Models\ParentModel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class WhatsappInviteBundleService
|
||||
{
|
||||
public function bundleForParent(int $pickedId, array $linkBySection, string $schoolYear, string $semester): ?array
|
||||
{
|
||||
$isPrimary = User::query()->whereKey($pickedId)->exists();
|
||||
$primaryId = $pickedId;
|
||||
|
||||
if (!$isPrimary) {
|
||||
$map = ParentModel::query()
|
||||
->select('firstparent_id', 'id')
|
||||
->where('id', $pickedId)
|
||||
->first();
|
||||
if (!$map || !$map->firstparent_id) {
|
||||
return null;
|
||||
}
|
||||
$primaryId = (int) $map->firstparent_id;
|
||||
}
|
||||
|
||||
$primary = User::query()
|
||||
->select('id', 'firstname', 'lastname', 'email')
|
||||
->whereKey($primaryId)
|
||||
->first();
|
||||
if (!$primary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$secondary = ParentModel::query()
|
||||
->select('id', 'secondparent_firstname', 'secondparent_lastname', 'secondparent_email')
|
||||
->where('firstparent_id', $primaryId)
|
||||
->where('id', '>', 0)
|
||||
->first();
|
||||
|
||||
$sectionRows = DB::table('student_class as sc')
|
||||
->select(DB::raw('DISTINCT sc.class_section_id'))
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->where('s.parent_id', $primaryId)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$sections = [];
|
||||
$linksSet = [];
|
||||
foreach ($sectionRows as $row) {
|
||||
$cid = (int) ($row->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;
|
||||
}
|
||||
|
||||
$emails = [];
|
||||
if (!empty($primary->email)) {
|
||||
$emails[$primary->email] = true;
|
||||
}
|
||||
if (!empty($secondary?->secondparent_email)) {
|
||||
$emails[$secondary->secondparent_email] = true;
|
||||
}
|
||||
if (empty($emails)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'parent' => [
|
||||
'id' => (int) $primary->id,
|
||||
'firstname' => (string) ($primary->firstname ?? ''),
|
||||
'lastname' => (string) ($primary->lastname ?? ''),
|
||||
'email' => (string) ($primary->email ?? ''),
|
||||
'source' => 'users',
|
||||
],
|
||||
'secondary_parents' => $secondary && !empty($secondary->id) ? [[
|
||||
'id' => (int) $secondary->id,
|
||||
'firstname' => (string) ($secondary->secondparent_firstname ?? ''),
|
||||
'lastname' => (string) ($secondary->secondparent_lastname ?? ''),
|
||||
'email' => (string) ($secondary->secondparent_email ?? ''),
|
||||
'source' => 'parents',
|
||||
]] : [],
|
||||
'sections' => $sections,
|
||||
'links' => array_keys($linksSet),
|
||||
'emails' => array_keys($emails),
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
}
|
||||
|
||||
public function bundlesForClass(int $classSectionId, array $linkBySection, string $schoolYear, string $semester): array
|
||||
{
|
||||
$link = (string) ($linkBySection[$classSectionId]['invite_link'] ?? '');
|
||||
if ($link === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$studentRows = DB::table('student_class')
|
||||
->select('student_id')
|
||||
->where('school_year', $schoolYear)
|
||||
->when($semester !== '', fn ($q) => $q->where('semester', $semester))
|
||||
->where('class_section_id', $classSectionId)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
if (empty($studentRows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$studentIds = array_values(array_unique(array_map(fn ($r) => (int) ($r->student_id ?? 0), $studentRows)));
|
||||
if (empty($studentIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parentRows = DB::table('students')
|
||||
->select('id', 'parent_id')
|
||||
->whereIn('id', $studentIds)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$parentIds = [];
|
||||
foreach ($parentRows as $row) {
|
||||
$pid = (int) ($row->parent_id ?? 0);
|
||||
if ($pid) {
|
||||
$parentIds[$pid] = true;
|
||||
}
|
||||
}
|
||||
$parentIds = array_keys($parentIds);
|
||||
if (empty($parentIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$primaryRows = DB::table('users')
|
||||
->select('id', 'firstname', 'lastname', 'email')
|
||||
->whereIn('id', $parentIds)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$profiles = [];
|
||||
foreach ($primaryRows as $row) {
|
||||
$uid = (int) ($row->id ?? 0);
|
||||
$profiles[$uid] = [
|
||||
'id' => $uid,
|
||||
'firstname' => (string) ($row->firstname ?? ''),
|
||||
'lastname' => (string) ($row->lastname ?? ''),
|
||||
'email' => (string) ($row->email ?? ''),
|
||||
'source' => 'users',
|
||||
];
|
||||
}
|
||||
|
||||
$secondaryRows = DB::table('parents')
|
||||
->select('firstparent_id', 'id', 'secondparent_firstname', 'secondparent_lastname', 'secondparent_email')
|
||||
->where('id', '>', 0)
|
||||
->whereIn('firstparent_id', $parentIds)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$secondaryByPrimary = [];
|
||||
foreach ($secondaryRows as $row) {
|
||||
$fp = (int) ($row->firstparent_id ?? 0);
|
||||
$secondaryByPrimary[$fp] = [
|
||||
'id' => (int) ($row->id ?? 0),
|
||||
'firstname' => (string) ($row->secondparent_firstname ?? ''),
|
||||
'lastname' => (string) ($row->secondparent_lastname ?? ''),
|
||||
'email' => (string) ($row->secondparent_email ?? ''),
|
||||
'source' => 'parents',
|
||||
];
|
||||
}
|
||||
|
||||
$bundles = [];
|
||||
foreach ($parentIds as $pid) {
|
||||
$profile = $profiles[$pid] ?? null;
|
||||
if (!$profile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$emails = [];
|
||||
if (!empty($profile['email'])) {
|
||||
$emails[$profile['email']] = true;
|
||||
}
|
||||
if (!empty($secondaryByPrimary[$pid]['email'])) {
|
||||
$emails[$secondaryByPrimary[$pid]['email']] = true;
|
||||
}
|
||||
if (empty($emails)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bundles[] = [
|
||||
'parent' => $profile,
|
||||
'secondary_parents' => !empty($secondaryByPrimary[$pid]['id']) ? [$secondaryByPrimary[$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),
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
}
|
||||
|
||||
return $bundles;
|
||||
}
|
||||
|
||||
public function bundlesForAll(array $linkBySection, string $schoolYear, string $semester): array
|
||||
{
|
||||
$sectionRows = DB::table('student_class')
|
||||
->select(DB::raw('DISTINCT class_section_id'))
|
||||
->where('school_year', $schoolYear)
|
||||
->when($semester !== '', fn ($q) => $q->where('semester', $semester))
|
||||
->orderBy('class_section_id', 'ASC')
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$bundles = [];
|
||||
foreach ($sectionRows as $row) {
|
||||
$cid = (int) ($row->class_section_id ?? 0);
|
||||
if (!$cid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classBundles = $this->bundlesForClass($cid, $linkBySection, $schoolYear, $semester);
|
||||
foreach ($classBundles as $bundle) {
|
||||
$bundles[] = $bundle;
|
||||
}
|
||||
}
|
||||
|
||||
return $bundles;
|
||||
}
|
||||
|
||||
public function consolidateBundles(array $payloads): array
|
||||
{
|
||||
$normalizeEmails = 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);
|
||||
};
|
||||
|
||||
$mergeUnique = 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'])
|
||||
? '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 $bundle) {
|
||||
$parent = $bundle['parent'] ?? [];
|
||||
$pid = (int) ($parent['id'] ?? 0);
|
||||
$primaryEmails = $normalizeEmails((array) ($bundle['emails'] ?? []));
|
||||
$groupKey = $pid > 0 ? ('pid:' . $pid) : ('mail:' . implode(',', $primaryEmails));
|
||||
|
||||
if (!isset($grouped[$groupKey])) {
|
||||
$grouped[$groupKey] = [
|
||||
'parent' => $parent,
|
||||
'emails' => $primaryEmails,
|
||||
'sections' => (array) ($bundle['sections'] ?? []),
|
||||
'links' => (array) ($bundle['links'] ?? []),
|
||||
'teachers' => (array) ($bundle['teachers'] ?? []),
|
||||
'items' => (array) ($bundle['items'] ?? []),
|
||||
'secondary_parents' => (array) ($bundle['secondary_parents'] ?? []),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$grouped[$groupKey]['parent'] = !empty($parent) ? $parent : $grouped[$groupKey]['parent'];
|
||||
$grouped[$groupKey]['emails'] = $normalizeEmails(array_merge($grouped[$groupKey]['emails'], $primaryEmails));
|
||||
$grouped[$groupKey]['sections'] = $mergeSections($grouped[$groupKey]['sections'], (array) ($bundle['sections'] ?? []));
|
||||
$grouped[$groupKey]['links'] = $mergeLinks($grouped[$groupKey]['links'], (array) ($bundle['links'] ?? []));
|
||||
$grouped[$groupKey]['teachers'] = $mergeUnique($grouped[$groupKey]['teachers'], (array) ($bundle['teachers'] ?? []));
|
||||
$grouped[$groupKey]['items'] = $mergeLinks($grouped[$groupKey]['items'], (array) ($bundle['items'] ?? []));
|
||||
$grouped[$groupKey]['secondary_parents'] = $mergeUnique($grouped[$groupKey]['secondary_parents'], (array) ($bundle['secondary_parents'] ?? []));
|
||||
}
|
||||
|
||||
return array_values($grouped);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Whatsapp;
|
||||
|
||||
use App\Events\WhatsappInvitesSend;
|
||||
use App\Models\ParentModel;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WhatsappInviteNotificationService
|
||||
{
|
||||
public function dispatch(array $bundles, string $schoolYear, string $semester): array
|
||||
{
|
||||
$listenerCount = Event::hasListeners(WhatsappInvitesSend::class) ? 1 : 0;
|
||||
if ($listenerCount === 0) {
|
||||
Log::warning('WhatsappInvitesSend has no listeners registered.');
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'No event listener registered for WhatsappInvitesSend.',
|
||||
'triggered' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$normalize = 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);
|
||||
};
|
||||
|
||||
$triggered = 0;
|
||||
foreach ($bundles as $bundle) {
|
||||
try {
|
||||
$primaryEmails = $normalize((array) ($bundle['emails'] ?? []));
|
||||
$secondaryEmails = $this->extractSecondaryEmails($bundle, $normalize);
|
||||
|
||||
if (empty($secondaryEmails)) {
|
||||
Log::info('No secondary emails for WhatsApp invite.', [
|
||||
'parent_id' => $bundle['parent']['id'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
$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 invite: no recipient email(s).', [
|
||||
'parent_id' => $bundle['parent']['id'] ?? null,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
event(new WhatsappInvitesSend([
|
||||
'parent' => $bundle['parent'] ?? null,
|
||||
'sections' => $bundle['sections'] ?? [],
|
||||
'schoolYear' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'links' => $bundle['links'] ?? [],
|
||||
'teachers' => $bundle['teachers'] ?? [],
|
||||
'to_emails' => $to,
|
||||
'cc_emails' => $cc,
|
||||
]));
|
||||
|
||||
$triggered++;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('WhatsappInvitesSend dispatch failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'triggered' => $triggered,
|
||||
];
|
||||
}
|
||||
|
||||
private function extractSecondaryEmails(array $bundle, callable $normalize): array
|
||||
{
|
||||
$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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parent = $bundle['parent'] ?? [];
|
||||
foreach (['secondary_email', 'secondparent_email', 'sec_email', 'email_secondary'] as $key) {
|
||||
if (!empty($parent[$key])) {
|
||||
$collected[] = $parent[$key];
|
||||
}
|
||||
}
|
||||
if (!empty($parent['secondparent']['email'])) {
|
||||
$collected[] = $parent['secondparent']['email'];
|
||||
}
|
||||
|
||||
try {
|
||||
$primaryId = (int) ($parent['id'] ?? 0);
|
||||
if ($primaryId > 0) {
|
||||
$row = ParentModel::query()
|
||||
->select('secondparent_email')
|
||||
->where('firstparent_id', $primaryId)
|
||||
->orderByDesc('updated_at')
|
||||
->first();
|
||||
if (!empty($row?->secondparent_email)) {
|
||||
$collected[] = $row->secondparent_email;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Secondary email DB lookup failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $normalize($collected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Whatsapp;
|
||||
|
||||
use App\Models\WhatsappGroupLink;
|
||||
|
||||
class WhatsappInviteService
|
||||
{
|
||||
public function __construct(
|
||||
private WhatsappContextService $context,
|
||||
private WhatsappInviteBundleService $bundleService,
|
||||
private WhatsappInviteNotificationService $notificationService
|
||||
) {
|
||||
}
|
||||
|
||||
public function send(array $payload): array
|
||||
{
|
||||
$mode = trim((string) ($payload['mode'] ?? ''));
|
||||
$schoolYear = trim((string) ($payload['school_year'] ?? '')) ?: $this->context->schoolYear();
|
||||
$semester = array_key_exists('semester', $payload)
|
||||
? trim((string) ($payload['semester'] ?? ''))
|
||||
: $this->context->semester();
|
||||
|
||||
$links = WhatsappGroupLink::getAllForTerm($schoolYear, $semester);
|
||||
$linkBySection = [];
|
||||
foreach ($links as $link) {
|
||||
$cid = (int) ($link->class_section_id ?? 0);
|
||||
if ($cid && !isset($linkBySection[$cid])) {
|
||||
$linkBySection[$cid] = $link->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
$bundles = [];
|
||||
if ($mode === 'parents') {
|
||||
$parentIds = array_map('intval', (array) ($payload['parent_ids'] ?? []));
|
||||
foreach ($parentIds as $pickedId) {
|
||||
if ($pickedId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$bundle = $this->bundleService->bundleForParent($pickedId, $linkBySection, $schoolYear, $semester);
|
||||
if ($bundle && !empty($bundle['emails']) && !empty($bundle['links'])) {
|
||||
$bundles[] = $bundle;
|
||||
}
|
||||
}
|
||||
} elseif ($mode === 'class') {
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
$classBundles = $this->bundleService->bundlesForClass($classSectionId, $linkBySection, $schoolYear, $semester);
|
||||
foreach ($classBundles as $bundle) {
|
||||
if (!empty($bundle['emails']) && !empty($bundle['links'])) {
|
||||
$bundles[] = $bundle;
|
||||
}
|
||||
}
|
||||
} elseif ($mode === 'all') {
|
||||
$allBundles = $this->bundleService->bundlesForAll($linkBySection, $schoolYear, $semester);
|
||||
foreach ($allBundles as $bundle) {
|
||||
if (!empty($bundle['emails']) && !empty($bundle['links'])) {
|
||||
$bundles[] = $bundle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($bundles)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'No recipients found (missing links or emails).',
|
||||
'triggered' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$consolidated = $this->bundleService->consolidateBundles($bundles);
|
||||
$result = $this->notificationService->dispatch($consolidated, $schoolYear, $semester);
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
return $result + ['ok' => false];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'triggered' => (int) ($result['triggered'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Whatsapp;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\WhatsappGroupLink;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
class WhatsappLinkService
|
||||
{
|
||||
public function __construct(private WhatsappContextService $context)
|
||||
{
|
||||
}
|
||||
|
||||
public function paginate(array $filters): LengthAwarePaginator
|
||||
{
|
||||
$query = WhatsappGroupLink::query();
|
||||
|
||||
$schoolYear = trim((string) ($filters['school_year'] ?? ''));
|
||||
if ($schoolYear !== '') {
|
||||
$query->forYear($schoolYear);
|
||||
}
|
||||
|
||||
if (array_key_exists('semester', $filters)) {
|
||||
$semester = trim((string) ($filters['semester'] ?? ''));
|
||||
if ($semester !== '') {
|
||||
$query->forSemester($semester);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($filters['class_section_id'])) {
|
||||
$query->forSection((int) $filters['class_section_id']);
|
||||
}
|
||||
|
||||
if (array_key_exists('active', $filters) && $filters['active'] !== null && $filters['active'] !== '') {
|
||||
$query->where('active', (int) (bool) $filters['active']);
|
||||
}
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
$search = trim((string) $filters['search']);
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('class_section_name', 'like', '%' . $search . '%')
|
||||
->orWhere('invite_link', 'like', '%' . $search . '%')
|
||||
->orWhere('class_section_id', $search);
|
||||
});
|
||||
}
|
||||
|
||||
$sortBy = (string) ($filters['sort_by'] ?? 'updated_at');
|
||||
$sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$allowedSorts = ['class_section_id', 'class_section_name', 'school_year', 'semester', 'active', 'updated_at'];
|
||||
if (!in_array($sortBy, $allowedSorts, true)) {
|
||||
$sortBy = 'updated_at';
|
||||
}
|
||||
|
||||
$perPage = (int) ($filters['per_page'] ?? 20);
|
||||
$perPage = $perPage > 0 ? $perPage : 20;
|
||||
|
||||
return $query->orderBy($sortBy, $sortDir)->paginate($perPage);
|
||||
}
|
||||
|
||||
public function findOrFail(int $id): WhatsappGroupLink
|
||||
{
|
||||
$link = WhatsappGroupLink::query()->find($id);
|
||||
if (!$link) {
|
||||
throw new ModelNotFoundException('WhatsApp group link not found.');
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
public function upsert(array $payload): WhatsappGroupLink
|
||||
{
|
||||
$schoolYear = trim((string) ($payload['school_year'] ?? '')) ?: $this->context->schoolYear();
|
||||
$semester = trim((string) ($payload['semester'] ?? ''));
|
||||
$sectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
|
||||
$sectionName = trim((string) ($payload['class_section_name'] ?? ''));
|
||||
if ($sectionName === '' && $sectionId > 0) {
|
||||
$sectionName = $this->resolveSectionName($sectionId);
|
||||
}
|
||||
|
||||
$link = WhatsappGroupLink::upsertLinkForSection(
|
||||
$sectionId,
|
||||
$sectionName !== '' ? $sectionName : ('Section ' . $sectionId),
|
||||
$schoolYear,
|
||||
$semester,
|
||||
(string) ($payload['invite_link'] ?? ''),
|
||||
(bool) ($payload['active'] ?? true)
|
||||
);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
public function update(int $id, array $payload): WhatsappGroupLink
|
||||
{
|
||||
$link = $this->findOrFail($id);
|
||||
|
||||
if (array_key_exists('class_section_name', $payload)) {
|
||||
$name = trim((string) ($payload['class_section_name'] ?? ''));
|
||||
if ($name === '' && !empty($payload['class_section_id'])) {
|
||||
$name = $this->resolveSectionName((int) $payload['class_section_id']);
|
||||
}
|
||||
$payload['class_section_name'] = $name;
|
||||
}
|
||||
|
||||
$link->fill($payload);
|
||||
$link->save();
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$link = $this->findOrFail($id);
|
||||
$link->delete();
|
||||
}
|
||||
|
||||
private function resolveSectionName(int $sectionId): string
|
||||
{
|
||||
$name = ClassSection::getClassSectionNameBySectionId($sectionId);
|
||||
return $name !== null && $name !== '' ? $name : ('Section ' . $sectionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Whatsapp;
|
||||
|
||||
use App\Models\WhatsappGroupMembership;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class WhatsappMembershipService
|
||||
{
|
||||
public function updateMembership(array $payload, ?int $verifiedBy = null): array
|
||||
{
|
||||
if (!Schema::hasTable('whatsapp_group_memberships')) {
|
||||
Log::error('WhatsApp membership table missing.');
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'Membership table missing. Please run migrations.',
|
||||
];
|
||||
}
|
||||
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
if ($classSectionId <= 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'Invalid class section.',
|
||||
];
|
||||
}
|
||||
|
||||
$schoolYear = trim((string) ($payload['school_year'] ?? ''));
|
||||
$semester = trim((string) ($payload['semester'] ?? ''));
|
||||
$primaryId = (int) ($payload['primary_id'] ?? 0);
|
||||
$secondId = (int) ($payload['second_id'] ?? 0);
|
||||
$primaryMember = $payload['primary_member'] ?? null;
|
||||
$secondMember = $payload['second_member'] ?? null;
|
||||
|
||||
$saved = DB::transaction(function () use (
|
||||
$classSectionId,
|
||||
$schoolYear,
|
||||
$semester,
|
||||
$primaryId,
|
||||
$secondId,
|
||||
$primaryMember,
|
||||
$secondMember,
|
||||
$verifiedBy
|
||||
) {
|
||||
$saved = [];
|
||||
|
||||
if ($primaryId > 0) {
|
||||
$id = WhatsappGroupMembership::upsertMembership(
|
||||
$classSectionId,
|
||||
$schoolYear,
|
||||
$semester,
|
||||
WhatsappGroupMembership::TYPE_PRIMARY,
|
||||
$primaryId,
|
||||
(bool) $primaryMember,
|
||||
$verifiedBy
|
||||
);
|
||||
$saved[] = 'primary=' . $primaryId . ':' . ($primaryMember ? '1' : '0');
|
||||
if ($id <= 0) {
|
||||
Log::warning('Failed to upsert primary membership.', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'primary_id' => $primaryId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($secondId > 0) {
|
||||
$id = WhatsappGroupMembership::upsertMembership(
|
||||
$classSectionId,
|
||||
$schoolYear,
|
||||
$semester,
|
||||
WhatsappGroupMembership::TYPE_SECOND,
|
||||
$secondId,
|
||||
(bool) $secondMember,
|
||||
$verifiedBy
|
||||
);
|
||||
$saved[] = 'second=' . $secondId . ':' . ($secondMember ? '1' : '0');
|
||||
if ($id <= 0) {
|
||||
Log::warning('Failed to upsert second membership.', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'second_id' => $secondId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $saved;
|
||||
});
|
||||
|
||||
if (empty($saved)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'Nothing to save (missing parent IDs).',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'saved' => $saved,
|
||||
'term' => [
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user