update controllers logic
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Models\AuthorizedUser;
|
||||
use App\Models\User;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use App\Services\Email\EmailDispatchService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* CodeIgniter {@see \App\Controllers\View\AuthorizedUsersController} parity.
|
||||
*/
|
||||
class AuthorizedUsersManagementService
|
||||
{
|
||||
private const TOKEN_TTL_HOURS = 24;
|
||||
|
||||
public function __construct(
|
||||
private EmailDispatchService $mail,
|
||||
private ApplicationUrlService $urls,
|
||||
) {}
|
||||
|
||||
public function hashToken(string $plain): string
|
||||
{
|
||||
return hash('sha256', $plain);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI create() first step — lookup by token after invite email (pending row, created within TTL).
|
||||
*/
|
||||
public function findPendingByIncomingToken(string $plainToken): ?AuthorizedUser
|
||||
{
|
||||
if ($plainToken === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hash = $this->hashToken($plainToken);
|
||||
|
||||
return AuthorizedUser::query()
|
||||
->where(function ($q) use ($hash, $plainToken) {
|
||||
$q->where('token', $hash)->orWhere('token', $plainToken);
|
||||
})
|
||||
->where('created_at', '>=', now()->subHours(self::TOKEN_TTL_HOURS))
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* CI setPassword / savePassword — active row with rotating token.
|
||||
*/
|
||||
public function findActiveSetupRow(int $authorizedUserId, string $plainToken): ?AuthorizedUser
|
||||
{
|
||||
if ($plainToken === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hash = $this->hashToken($plainToken);
|
||||
|
||||
return AuthorizedUser::query()
|
||||
->where(function ($q) use ($hash, $plainToken) {
|
||||
$q->where('token', $hash)->orWhere('token', $plainToken);
|
||||
})
|
||||
->where('authorized_user_id', $authorizedUserId)
|
||||
->where('status', 'Active')
|
||||
->where('updated_at', '>=', now()->subHours(self::TOKEN_TTL_HOURS))
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{redirect_url: string}
|
||||
*/
|
||||
public function confirmEmailClick(string $plainToken): array
|
||||
{
|
||||
$row = $this->findPendingByIncomingToken($plainToken);
|
||||
if (! $row) {
|
||||
throw new \InvalidArgumentException('Invalid or expired confirmation link.');
|
||||
}
|
||||
|
||||
$nextPlain = bin2hex(random_bytes(48));
|
||||
$nextHash = $this->hashToken($nextPlain);
|
||||
|
||||
$row->status = 'Active';
|
||||
$row->token = $nextHash;
|
||||
$row->save();
|
||||
|
||||
$url = $this->urls->inviteSetPasswordFormUrl((int) $row->authorized_user_id, $nextPlain);
|
||||
|
||||
return ['redirect_url' => $url];
|
||||
}
|
||||
|
||||
/**
|
||||
* CI `create` — when email is not a registered user, respond success without leaking (privacy).
|
||||
*
|
||||
* @return array{created: bool, record: AuthorizedUser|null}
|
||||
*/
|
||||
public function inviteByEmail(int $primaryUserId, string $email): array
|
||||
{
|
||||
$email = strtolower(trim($email));
|
||||
|
||||
$user = User::query()
|
||||
->whereRaw('LOWER(email) = ?', [$email])
|
||||
->first();
|
||||
|
||||
if (! $user) {
|
||||
return ['created' => false, 'record' => null];
|
||||
}
|
||||
|
||||
$plain = bin2hex(random_bytes(48));
|
||||
$tokenHash = $this->hashToken($plain);
|
||||
|
||||
$phone = trim((string) ($user->cellphone ?? ''));
|
||||
$record = AuthorizedUser::query()->create([
|
||||
'user_id' => $primaryUserId,
|
||||
'authorized_user_id' => (int) $user->id,
|
||||
'firstname' => (string) ($user->firstname ?? ''),
|
||||
'lastname' => (string) ($user->lastname ?? ''),
|
||||
'phone_number' => $phone !== '' ? $phone : '-',
|
||||
'gender' => trim((string) ($user->gender ?? '')) !== '' ? (string) $user->gender : '-',
|
||||
'email' => $email,
|
||||
'relation_to_user' => null,
|
||||
'token' => $tokenHash,
|
||||
'status' => 'Pending',
|
||||
]);
|
||||
|
||||
$this->sendConfirmationEmail($email, $plain);
|
||||
|
||||
return ['created' => true, 'record' => $record];
|
||||
}
|
||||
|
||||
public function sendConfirmationEmail(string $email, string $plainToken): void
|
||||
{
|
||||
$confirmLink = $this->urls->inviteConfirmUrl($plainToken);
|
||||
$body = '<p>You have been added as an authorized user for another account. Click the link below to confirm your access:</p>'
|
||||
.'<p><a href="'.e($confirmLink).'">Confirm Access</a></p>'
|
||||
.'<p>If you did not request this, please ignore this email.</p>';
|
||||
|
||||
$this->mail->send($email, 'Authorized User Confirmation', $body, 'notifications');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setAuthorizedUserPassword(int $authorizedUserId, int $postedUserId, string $plainToken, string $password): void
|
||||
{
|
||||
if ($postedUserId !== $authorizedUserId || $authorizedUserId <= 0) {
|
||||
throw new \InvalidArgumentException('Invalid request.');
|
||||
}
|
||||
|
||||
$row = $this->findActiveSetupRow($authorizedUserId, $plainToken);
|
||||
if (! $row) {
|
||||
throw new \InvalidArgumentException('Invalid or expired confirmation link.');
|
||||
}
|
||||
|
||||
$user = User::query()->find($authorizedUserId);
|
||||
if (! $user) {
|
||||
throw new \InvalidArgumentException('User not found.');
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $row, $password) {
|
||||
$user->password = $password;
|
||||
$user->save();
|
||||
|
||||
$row->token = null;
|
||||
$row->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ namespace App\Services\Parents;
|
||||
|
||||
use App\Models\AttendanceData;
|
||||
use App\Models\AttendanceRecord;
|
||||
use App\Models\EarlyDismissalSignature;
|
||||
use App\Models\ParentAttendanceReport;
|
||||
use App\Models\Student;
|
||||
use App\Models\ClassSection;
|
||||
use App\Services\SemesterRangeService;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ParentAttendanceReportService
|
||||
@@ -254,12 +256,12 @@ class ParentAttendanceReportService
|
||||
];
|
||||
}
|
||||
|
||||
public function listReports(string $start, ?string $end, ?string $schoolYear, ?string $semester): array
|
||||
public function listReports(string $start, ?string $end, ?string $schoolYear, ?string $semester, int $parentId): array
|
||||
{
|
||||
return ParentAttendanceReport::listForDateRange($start, $end, $schoolYear, $semester);
|
||||
return ParentAttendanceReport::listForDateRange($start, $end, $schoolYear, $semester, $parentId);
|
||||
}
|
||||
|
||||
public function checkExisting(array $payload): array
|
||||
public function checkExisting(int $parentId, array $payload): array
|
||||
{
|
||||
$datesPost = $payload['dates'] ?? null;
|
||||
$dateSingle = $payload['date'] ?? null;
|
||||
@@ -286,6 +288,15 @@ class ParentAttendanceReportService
|
||||
return [];
|
||||
}
|
||||
|
||||
$uniqueIds = array_values(array_unique($studentIds));
|
||||
$ownedCount = Student::query()
|
||||
->where('parent_id', $parentId)
|
||||
->whereIn('id', $uniqueIds)
|
||||
->count();
|
||||
if ($ownedCount !== count($uniqueIds)) {
|
||||
throw new \RuntimeException('Invalid student selection.');
|
||||
}
|
||||
|
||||
$rows = DB::table('parent_attendance_reports as par')
|
||||
->select('par.student_id', 'par.type', 'par.report_date', 's.firstname', 's.lastname')
|
||||
->leftJoin('students as s', 's.id', '=', 'par.student_id')
|
||||
@@ -371,6 +382,275 @@ class ParentAttendanceReportService
|
||||
$row->update($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Staff index: early dismissal rows grouped by date (+ signature metadata per date).
|
||||
*
|
||||
* Parity with CodeIgniter {@see \App\Controllers\View\ParentAttendanceReportController::earlyDismissals}.
|
||||
*
|
||||
* @return array{groups: array<string, array<int, array<string, mixed>>>, school_year: string, semester: string, signature_by_date: array<string, array<string, mixed>>}
|
||||
*/
|
||||
public function staffEarlyDismissalsIndex(?string $schoolYearParam, ?string $semesterParam): array
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$schoolYear = filled($schoolYearParam) ? trim((string) $schoolYearParam) : (string) ($context['school_year'] ?? '');
|
||||
$semester = filled($semesterParam) ? trim((string) $semesterParam) : '';
|
||||
|
||||
$b = DB::table('parent_attendance_reports as par')
|
||||
->select('par.*', 's.firstname', 's.lastname', 'cs.class_section_name')
|
||||
->leftJoin('students as s', 's.id', '=', 'par.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'par.class_section_id')
|
||||
->where('par.type', '=', 'early_dismissal');
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$b->where('par.school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$b->where('par.semester', $semester);
|
||||
}
|
||||
|
||||
$rows = $b->orderByDesc('par.report_date')
|
||||
->orderBy('par.dismiss_time')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
$groups = [];
|
||||
foreach ($rows as $r) {
|
||||
$d = substr((string) ($r['report_date'] ?? ''), 0, 10);
|
||||
if ($d === '') {
|
||||
$d = 'Unknown';
|
||||
}
|
||||
$groups[$d][] = $r;
|
||||
}
|
||||
uksort($groups, static function ($a, $b) {
|
||||
if ($a === 'Unknown' && $b === 'Unknown') {
|
||||
return 0;
|
||||
}
|
||||
if ($a === 'Unknown') {
|
||||
return 1;
|
||||
}
|
||||
if ($b === 'Unknown') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return strcmp((string) $b, (string) $a);
|
||||
});
|
||||
|
||||
$signatureByDate = [];
|
||||
$dates = array_values(array_filter(array_keys($groups), static function ($d) {
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $d) === 1;
|
||||
}));
|
||||
if ($dates !== []) {
|
||||
$sigQ = EarlyDismissalSignature::query()->whereIn('report_date', $dates);
|
||||
if ($schoolYear !== '') {
|
||||
$sigQ->where('school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$sigQ->where('semester', $semester);
|
||||
}
|
||||
foreach ($sigQ->orderBy('report_date')->get() as $sig) {
|
||||
$d = $sig->report_date instanceof \DateTimeInterface
|
||||
? $sig->report_date->format('Y-m-d')
|
||||
: substr((string) $sig->report_date, 0, 10);
|
||||
if ($d !== '') {
|
||||
$signatureByDate[$d] = $sig->toArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'groups' => $groups,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'signature_by_date' => $signatureByDate,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parity with CodeIgniter {@see \App\Controllers\View\ParentAttendanceReportController::addEarlyDismissalForm}.
|
||||
*
|
||||
* @return array{students: array<int, array<string, mixed>>, default_date: string, school_year: string, semester: string}
|
||||
*/
|
||||
public function staffAddEarlyDismissalFormData(): array
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$semester = (string) ($context['semester'] ?? '');
|
||||
|
||||
try {
|
||||
$students = DB::table('students as s')
|
||||
->select('s.id', 's.firstname', 's.lastname', 's.parent_id', 'cs.class_section_name', 'sc.class_section_id')
|
||||
->leftJoin('student_class as sc', function ($join) use ($schoolYear) {
|
||||
$join->on('sc.student_id', '=', 's.id')
|
||||
->where('sc.school_year', '=', $schoolYear);
|
||||
})
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->orderBy('s.lastname')
|
||||
->orderBy('s.firstname')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
} catch (\Throwable) {
|
||||
$students = [];
|
||||
}
|
||||
|
||||
[, $defaultDate] = $this->calendarService->computeSundays(0, 26);
|
||||
|
||||
return [
|
||||
'students' => $students,
|
||||
'default_date' => $defaultDate,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parity with CodeIgniter {@see \App\Controllers\View\ParentAttendanceReportController::saveEarlyDismissal}.
|
||||
*/
|
||||
public function saveStaffEarlyDismissal(int $studentId, string $reportDate, string $dismissTime, string $reason): void
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$semester = (string) ($context['semester'] ?? '');
|
||||
|
||||
$dt = \DateTime::createFromFormat('Y-m-d', $reportDate);
|
||||
if (! $dt || (int) $dt->format('w') !== 0) {
|
||||
throw new \RuntimeException('Please select a Sunday date.');
|
||||
}
|
||||
|
||||
$stu = Student::query()->select('id', 'parent_id', 'firstname', 'lastname')->find($studentId);
|
||||
if (! $stu || empty($stu->parent_id)) {
|
||||
throw new \RuntimeException('Selected student not found or has no primary parent assigned.');
|
||||
}
|
||||
$parentId = (int) $stu->parent_id;
|
||||
|
||||
$sectionRow = DB::table('student_class')
|
||||
->select('class_section_id')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('updated_at')
|
||||
->first();
|
||||
|
||||
$classSectionId = $sectionRow ? (int) $sectionRow->class_section_id : null;
|
||||
|
||||
$qb = ParentAttendanceReport::query()
|
||||
->where('student_id', $studentId)
|
||||
->whereDate('report_date', $reportDate)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
$existingAl = (clone $qb)->where(function ($q) {
|
||||
$q->where('type', 'absent')->orWhere('type', 'late');
|
||||
})->first();
|
||||
|
||||
if ($existingAl) {
|
||||
throw new \RuntimeException('Absent/Late already submitted for this student on the selected date.');
|
||||
}
|
||||
|
||||
$existingEd = (clone $qb)->where('type', 'early_dismissal')->first();
|
||||
|
||||
if ($existingEd) {
|
||||
$existingEd->update([
|
||||
'dismiss_time' => $dismissTime !== '' ? $dismissTime : $existingEd->dismiss_time,
|
||||
'reason' => $reason !== '' ? $reason : $existingEd->reason,
|
||||
'status' => 'new',
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ParentAttendanceReport::query()->create([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'report_date' => $reportDate,
|
||||
'type' => 'early_dismissal',
|
||||
'dismiss_time' => $dismissTime,
|
||||
'reason' => $reason !== '' ? $reason : null,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'status' => 'new',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parity with CodeIgniter {@see \App\Controllers\View\ParentAttendanceReportController::uploadEarlyDismissalSignature}.
|
||||
* Files live under {@see storage_path('uploads/early_dismissal_signatures')} (same as {@see \App\Http\Controllers\Api\Reports\FilesController::earlyDismissalSignature}).
|
||||
*/
|
||||
public function storeEarlyDismissalSignature(
|
||||
UploadedFile $file,
|
||||
string $reportDate,
|
||||
?string $schoolYearPost,
|
||||
?string $semesterPost,
|
||||
int $uploadedByUserId,
|
||||
): void {
|
||||
$dt = \DateTime::createFromFormat('Y-m-d', $reportDate);
|
||||
if (! $dt || (int) $dt->format('w') !== 0) {
|
||||
throw new \RuntimeException('Please select a Sunday date.');
|
||||
}
|
||||
|
||||
$context = $this->configService->context();
|
||||
$schoolYear = $schoolYearPost !== null && $schoolYearPost !== ''
|
||||
? (string) $schoolYearPost
|
||||
: (string) ($context['school_year'] ?? '');
|
||||
$semesterRaw = (string) ($semesterPost ?? '');
|
||||
$hasSemesterFilter = $semesterRaw !== '';
|
||||
$semester = $hasSemesterFilter ? $semesterRaw : null;
|
||||
|
||||
$existsQ = ParentAttendanceReport::query()
|
||||
->where('type', 'early_dismissal')
|
||||
->whereDate('report_date', $reportDate);
|
||||
if ($schoolYear !== '') {
|
||||
$existsQ->where('school_year', $schoolYear);
|
||||
}
|
||||
if ($hasSemesterFilter) {
|
||||
$existsQ->where('semester', $semester);
|
||||
}
|
||||
if (! $existsQ->exists()) {
|
||||
throw new \RuntimeException('No early dismissal records found for that date.');
|
||||
}
|
||||
|
||||
$targetDir = storage_path('uploads/early_dismissal_signatures');
|
||||
if (! is_dir($targetDir) && ! @mkdir($targetDir, 0755, true) && ! is_dir($targetDir)) {
|
||||
throw new \RuntimeException('Upload directory is not available.');
|
||||
}
|
||||
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$mimeType = $file->getClientMimeType();
|
||||
$fileSize = (int) $file->getSize();
|
||||
$filename = $file->hashName();
|
||||
$file->move($targetDir, $filename);
|
||||
|
||||
$payload = [
|
||||
'report_date' => $reportDate,
|
||||
'school_year' => $schoolYear !== '' ? $schoolYear : null,
|
||||
'semester' => $semester,
|
||||
'filename' => $filename,
|
||||
'original_name' => $originalName,
|
||||
'mime_type' => $mimeType,
|
||||
'file_size' => $fileSize,
|
||||
'uploaded_by' => $uploadedByUserId,
|
||||
];
|
||||
|
||||
$existingQ = EarlyDismissalSignature::query()->whereDate('report_date', $reportDate);
|
||||
if ($schoolYear !== '') {
|
||||
$existingQ->where('school_year', $schoolYear);
|
||||
}
|
||||
if ($hasSemesterFilter) {
|
||||
$existingQ->where('semester', $semester);
|
||||
}
|
||||
$existing = $existingQ->first();
|
||||
|
||||
if ($existing) {
|
||||
$oldFile = (string) ($existing->filename ?? '');
|
||||
$existing->update($payload);
|
||||
if ($oldFile !== '' && $oldFile !== $filename) {
|
||||
@unlink($targetDir . DIRECTORY_SEPARATOR . $oldFile);
|
||||
}
|
||||
} else {
|
||||
EarlyDismissalSignature::query()->create($payload);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveClassSectionLabel(?int $classSectionId): string
|
||||
{
|
||||
$cid = (int) ($classSectionId ?? 0);
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Services\ClassProgress\ClassProgressQueryService;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* CodeIgniter {@see \App\Controllers\ParentProgressController} read-side logic.
|
||||
*/
|
||||
class ParentProgressQueryService
|
||||
{
|
||||
public function __construct(
|
||||
private ClassProgressQueryService $classProgressQuery,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* CI `getParentStudents` — one row per distinct student (latest enrollment ordering).
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function parentStudents(int $parentId): array
|
||||
{
|
||||
if ($parentId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = DB::table('enrollments as e')
|
||||
->select(
|
||||
'e.student_id',
|
||||
'e.class_section_id',
|
||||
'e.updated_at',
|
||||
'e.created_at',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
'cs.class_section_name',
|
||||
)
|
||||
->join('students as s', 's.id', '=', 'e.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'e.class_section_id')
|
||||
->where('e.parent_id', $parentId)
|
||||
->where('e.is_withdrawn', 0)
|
||||
->orderByDesc('e.updated_at')
|
||||
->orderByDesc('e.created_at')
|
||||
->get();
|
||||
|
||||
$students = [];
|
||||
foreach ($rows as $row) {
|
||||
$arr = (array) $row;
|
||||
$studentId = (int) ($arr['student_id'] ?? 0);
|
||||
if ($studentId === 0 || isset($students[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$students[$studentId] = $arr;
|
||||
}
|
||||
|
||||
return array_values($students);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI `getParentSectionIds`.
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
public function parentSectionIds(int $parentId): array
|
||||
{
|
||||
if ($parentId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ids = DB::table('enrollments')
|
||||
->where('parent_id', $parentId)
|
||||
->where('is_withdrawn', 0)
|
||||
->whereNotNull('class_section_id')
|
||||
->distinct()
|
||||
->pluck('class_section_id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->filter(fn ($id) => $id > 0)
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return array_values($ids);
|
||||
}
|
||||
|
||||
public function parentCanAccessSection(int $parentUserId, ?int $classSectionId): bool
|
||||
{
|
||||
if (! $classSectionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array((int) $classSectionId, $this->parentSectionIds($parentUserId), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress reports visible to the parent (all teachers) for allowed sections.
|
||||
*
|
||||
* @param list<int> $sectionIds
|
||||
*/
|
||||
public function reportsForSections(array $sectionIds): Collection
|
||||
{
|
||||
$sectionIds = array_values(array_filter(array_map('intval', $sectionIds)));
|
||||
if ($sectionIds === []) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return ClassProgressReport::query()
|
||||
->select('class_progress_reports.*')
|
||||
->addSelect([
|
||||
'cs.class_section_name',
|
||||
DB::raw('CONCAT(IFNULL(u.firstname, ""), " ", IFNULL(u.lastname, "")) AS teacher_name'),
|
||||
])
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'class_progress_reports.class_section_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 'class_progress_reports.teacher_id')
|
||||
->whereIn('class_progress_reports.class_section_id', $sectionIds)
|
||||
->orderByDesc('class_progress_reports.week_start')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* CI `groupReportsByWeek` — keys `"{week_start}:{class_section_id}"`.
|
||||
*
|
||||
* @param iterable<int, ClassProgressReport> $rows
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function groupReportsByWeek(iterable $rows): array
|
||||
{
|
||||
$statusOptions = (array) config('progress.status_options', []);
|
||||
$reportGroups = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statusLabel = $statusOptions[$row->status] ?? 'Unknown';
|
||||
$row->setAttribute('status_label', $statusLabel);
|
||||
|
||||
$weekStart = $row->week_start instanceof \Carbon\CarbonInterface
|
||||
? $row->week_start->format('Y-m-d')
|
||||
: (string) $row->week_start;
|
||||
$sectionId = (int) $row->class_section_id;
|
||||
if ($weekStart === '' || $sectionId === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $weekStart.':'.$sectionId;
|
||||
if (! isset($reportGroups[$key])) {
|
||||
$reportGroups[$key] = [
|
||||
'week_start' => $weekStart,
|
||||
'week_end' => $row->week_end instanceof \Carbon\CarbonInterface
|
||||
? $row->week_end->format('Y-m-d')
|
||||
: (string) ($row->week_end ?? ''),
|
||||
'class_section_name' => $row->class_section_name ?? '',
|
||||
'class_section_id' => $sectionId,
|
||||
'reports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$subject = (string) ($row->subject ?? '');
|
||||
if ($subject === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reportGroups[$key]['reports'][$subject] = (new \App\Http\Resources\ClassProgress\ClassProgressReportResource($row))->toArray(request());
|
||||
}
|
||||
|
||||
return $reportGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Weekly batch for a section/week (CI parent `view`, all subjects).
|
||||
*
|
||||
* @return list<ClassProgressReport>
|
||||
*/
|
||||
public function weeklyReportsForSectionWeek(int $classSectionId, string $weekStartYmd): array
|
||||
{
|
||||
return ClassProgressReport::query()
|
||||
->with(['classSection', 'teacher'])
|
||||
->where('class_section_id', $classSectionId)
|
||||
->whereDate('week_start', $weekStartYmd)
|
||||
->orderBy('subject')
|
||||
->get()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, list<array{id:int,name:string}>>
|
||||
*/
|
||||
public function attachmentMapForReportIds(array $reportIds): array
|
||||
{
|
||||
return $this->classProgressQuery->attachmentMap($reportIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use App\Models\ReportCardAcknowledgement;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* CodeIgniter {@see \App\Controllers\ParentReportCardController} parity (read + acknowledgement).
|
||||
*/
|
||||
class ParentReportCardService
|
||||
{
|
||||
public function __construct(
|
||||
private PrimaryParentUserResolver $primaryParentResolver,
|
||||
private ApplicationUrlService $urls,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Resolve term from query string with configuration fallbacks (CI index/view).
|
||||
*
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
public function termFromRequest(?string $schoolYearGet, ?string $semesterGet): array
|
||||
{
|
||||
$schoolYear = trim((string) ($schoolYearGet ?? ''));
|
||||
$semester = trim((string) ($semesterGet ?? ''));
|
||||
if ($schoolYear === '') {
|
||||
$schoolYear = trim((string) (Configuration::getConfigValueByKey('school_year') ?? ''));
|
||||
}
|
||||
if ($semester === '') {
|
||||
$semester = trim((string) (Configuration::getConfigValueByKey('semester') ?? ''));
|
||||
}
|
||||
|
||||
return [$schoolYear, $semester];
|
||||
}
|
||||
|
||||
/**
|
||||
* CI sign() — current configuration term only (no GET overrides).
|
||||
*
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
public function termFromConfiguration(): array
|
||||
{
|
||||
$schoolYear = trim((string) (Configuration::getConfigValueByKey('school_year') ?? ''));
|
||||
$semester = trim((string) (Configuration::getConfigValueByKey('semester') ?? ''));
|
||||
|
||||
return [$schoolYear, $semester];
|
||||
}
|
||||
|
||||
public function resolvePrimaryParentUserId(?User $user): ?int
|
||||
{
|
||||
return $this->primaryParentResolver->resolveFromUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI index() student rows for parent + optional student_class term filters.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function studentsForTerm(int $primaryParentUserId, string $schoolYear, string $semester): array
|
||||
{
|
||||
if ($primaryParentUserId <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$q = DB::table('students as s')
|
||||
->select('s.id', 's.firstname', 's.lastname', 'cs.class_section_name')
|
||||
->leftJoin('student_class as sc', 'sc.student_id', '=', 's.id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('s.parent_id', $primaryParentUserId)
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC');
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$q->where('sc.school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$q->where('sc.semester', $semester);
|
||||
}
|
||||
|
||||
return $q->get()->map(static fn ($r) => (array) $r)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $studentIds
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function acknowledgementMap(int $primaryParentUserId, array $studentIds, string $schoolYear, string $semester): array
|
||||
{
|
||||
if ($primaryParentUserId <= 0 || $studentIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = ReportCardAcknowledgement::query()
|
||||
->where('parent_id', $primaryParentUserId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->whereIn('student_id', $studentIds)
|
||||
->get();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$map[(int) $row->student_id] = $row->toArray();
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function studentBelongsToPrimaryParent(int $studentId, int $primaryParentUserId): bool
|
||||
{
|
||||
$pid = DB::table('students')->where('id', $studentId)->value('parent_id');
|
||||
|
||||
return $pid !== null && (int) $pid === $primaryParentUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $values viewed_at, signed_at, signed_name, signer_ip, …
|
||||
*/
|
||||
public function touchAcknowledgement(
|
||||
int $primaryParentUserId,
|
||||
int $studentId,
|
||||
string $schoolYear,
|
||||
string $semester,
|
||||
array $values,
|
||||
): void {
|
||||
$criteria = [
|
||||
'parent_id' => $primaryParentUserId,
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
];
|
||||
|
||||
$existing = ReportCardAcknowledgement::query()->where($criteria)->first();
|
||||
if ($existing) {
|
||||
$existing->fill($values);
|
||||
$existing->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ReportCardAcknowledgement::query()->create(array_merge($criteria, $values));
|
||||
}
|
||||
|
||||
public function reportCardUrl(int $studentId, string $schoolYear, string $semester): string
|
||||
{
|
||||
$query = [];
|
||||
if ($schoolYear !== '') {
|
||||
$query['school_year'] = $schoolYear;
|
||||
}
|
||||
if ($semester !== '') {
|
||||
$query['semester'] = $semester;
|
||||
}
|
||||
|
||||
$base = $this->urls->studentReportCardUrl($studentId);
|
||||
|
||||
return $query === [] ? $base : $base.'?'.http_build_query($query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Parents;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Maps the logged-in parent-family account to the primary parent's user id
|
||||
* (stored on {@see \App\Models\Student::$parent_id}), matching CodeIgniter
|
||||
* {@see \App\Controllers\ParentReportCardController::resolvePrimaryParentId}.
|
||||
*/
|
||||
final class PrimaryParentUserResolver
|
||||
{
|
||||
public function resolveFromUser(?User $user): ?int
|
||||
{
|
||||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->resolveFromCredentials((int) $user->id, (string) ($user->user_type ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $userId Session user id (may be secondary/tertiary login).
|
||||
*/
|
||||
public function resolveFromCredentials(int $userId, string $userType): ?int
|
||||
{
|
||||
$userType = strtolower(trim($userType));
|
||||
if ($userType === 'primary') {
|
||||
return $userId > 0 ? $userId : null;
|
||||
}
|
||||
|
||||
if ($userType === 'secondary') {
|
||||
$row = DB::table('parents')
|
||||
->select('parent_id')
|
||||
->where('secondparent_user_id', $userId)
|
||||
->first();
|
||||
|
||||
return $row ? (int) ($row->parent_id ?? 0) ?: null : null;
|
||||
}
|
||||
|
||||
if ($userType === 'tertiary') {
|
||||
$row = DB::table('authorized_users')
|
||||
->select('user_id')
|
||||
->where('authorized_user_id', $userId)
|
||||
->first();
|
||||
|
||||
return $row ? (int) ($row->user_id ?? 0) ?: null : null;
|
||||
}
|
||||
|
||||
return $userId > 0 ? $userId : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user