add more controller

This commit is contained in:
root
2026-03-10 00:48:32 -04:00
parent 5eeaec0257
commit 20ee70d153
151 changed files with 9481 additions and 8407 deletions
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Api\Administrator;
use App\Http\Controllers\Controller;
use App\Http\Requests\EmergencyContacts\EmergencyContactUpdateRequest;
use App\Http\Resources\EmergencyContacts\EmergencyContactGroupResource;
use App\Http\Resources\EmergencyContacts\EmergencyContactResource;
use App\Services\EmergencyContacts\EmergencyContactCrudService;
use App\Services\EmergencyContacts\EmergencyContactDirectoryService;
use Illuminate\Http\JsonResponse;
class EmergencyContactController extends Controller
{
public function __construct(
private EmergencyContactDirectoryService $directoryService,
private EmergencyContactCrudService $crudService
) {
}
public function index(): JsonResponse
{
$groups = $this->directoryService->groups();
return response()->json([
'ok' => true,
'groups' => EmergencyContactGroupResource::collection($groups),
]);
}
public function update(EmergencyContactUpdateRequest $request, int $contactId): JsonResponse
{
$contact = $this->crudService->update($contactId, $request->validated());
return response()->json([
'ok' => true,
'contact' => new EmergencyContactResource($contact),
]);
}
public function destroy(int $contactId): JsonResponse
{
$this->crudService->delete($contactId);
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api\Administrator;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Teachers\TeacherAssignmentDeleteRequest;
use App\Http\Requests\Teachers\TeacherAssignmentListRequest;
use App\Http\Requests\Teachers\TeacherAssignmentStoreRequest;
use App\Http\Resources\Teachers\TeacherAssignmentResource;
use App\Http\Resources\Teachers\TeacherClassResource;
use App\Services\Teachers\TeacherAssignmentService;
use Illuminate\Http\JsonResponse;
class TeacherClassAssignmentController extends BaseApiController
{
public function __construct(private TeacherAssignmentService $assignmentService)
{
parent::__construct();
}
public function index(TeacherAssignmentListRequest $request): JsonResponse
{
$data = $this->assignmentService->listAssignments($request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'school_year' => $data['school_year'],
'teachers' => TeacherAssignmentResource::collection($data['teachers']),
'classes' => TeacherClassResource::collection($data['classes']),
]);
}
public function store(TeacherAssignmentStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$payload['updated_by'] = (int) (auth()->id() ?? 0);
$result = $this->assignmentService->assign($payload);
$status = $result['ok'] ? 200 : 422;
return response()->json([
'ok' => (bool) $result['ok'],
'message' => $result['message'] ?? '',
'position' => $result['position'] ?? null,
], $status);
}
public function destroy(TeacherAssignmentDeleteRequest $request): JsonResponse
{
$result = $this->assignmentService->delete($request->validated());
$status = $result['ok'] ? 200 : 422;
return response()->json([
'ok' => (bool) $result['ok'],
'message' => $result['message'] ?? '',
], $status);
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\Api\Exams;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Exams\ExamDraftAdminLegacyRequest;
use App\Http\Requests\Exams\ExamDraftAdminReviewRequest;
use App\Http\Requests\Exams\ExamDraftTeacherStoreRequest;
use App\Http\Resources\Exams\ExamDraftResource;
use App\Services\Exams\ExamDraftAdminService;
use App\Services\Exams\ExamDraftTeacherService;
use Illuminate\Http\JsonResponse;
class ExamDraftController extends BaseApiController
{
public function __construct(
private ExamDraftTeacherService $teacherService,
private ExamDraftAdminService $adminService
) {
parent::__construct();
}
public function teacherIndex(): JsonResponse
{
$teacherId = (int) (auth()->id() ?? 0);
if ($teacherId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->teacherService->listForTeacher($teacherId);
return response()->json([
'ok' => true,
'assignments' => $data['assignments'],
'drafts' => ExamDraftResource::collection($data['drafts']),
'legacy_exams' => ExamDraftResource::collection($data['legacyExams']),
'exam_types' => $data['examTypes'],
'status_options' => $data['statusOptions'],
'school_year' => $data['schoolYear'],
'semester' => $data['semester'],
]);
}
public function teacherStore(ExamDraftTeacherStoreRequest $request): JsonResponse
{
$teacherId = (int) (auth()->id() ?? 0);
if ($teacherId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$result = $this->teacherService->store($teacherId, $request->validated(), $request->file('draft_file'));
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422);
}
return response()->json([
'ok' => true,
'draft' => new ExamDraftResource($result['draft']),
], 201);
}
public function adminIndex(): JsonResponse
{
$data = $this->adminService->list();
return response()->json([
'ok' => true,
'drafts' => ExamDraftResource::collection($data['drafts']),
'class_sections' => $data['classSections'],
'legacy_by_class' => $data['legacyByClass'],
'exam_types' => $data['examTypes'],
'status_options' => $data['statusOptions'],
'school_year' => $data['schoolYear'],
'semester' => $data['semester'],
'allowed_extensions' => $data['allowedExtensions'],
'max_upload_bytes' => $data['maxUploadBytes'],
]);
}
public function adminUploadLegacy(ExamDraftAdminLegacyRequest $request): JsonResponse
{
$adminId = (int) (auth()->id() ?? 0);
if ($adminId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$result = $this->adminService->uploadLegacy($adminId, $request->validated(), $request->file('old_exam_file'));
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422);
}
return response()->json([
'ok' => true,
'draft' => new ExamDraftResource($result['draft']),
], 201);
}
public function adminReview(ExamDraftAdminReviewRequest $request): JsonResponse
{
$adminId = (int) (auth()->id() ?? 0);
if ($adminId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$result = $this->adminService->review($adminId, $request->validated(), $request->file('final_file'));
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422);
}
return response()->json([
'ok' => true,
'draft' => new ExamDraftResource($result['draft']),
]);
}
}
@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Api\Parents;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Parents\ParentAttendanceReportSubmitRequest;
use App\Http\Requests\Parents\ParentAttendanceReportListRequest;
use App\Http\Requests\Parents\ParentAttendanceReportCheckRequest;
use App\Http\Requests\Parents\ParentAttendanceReportUpdateRequest;
use App\Http\Resources\Parents\ParentAttendanceReportResource;
use App\Services\Parents\ParentAttendanceReportService;
use Illuminate\Http\JsonResponse;
class ParentAttendanceReportController extends BaseApiController
{
public function __construct(private ParentAttendanceReportService $reportService)
{
parent::__construct();
}
public function form(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->reportService->formData($parentId);
return response()->json([
'ok' => true,
'students' => $data['students'],
'today' => $data['today'],
'sundays' => $data['sundays'],
'defaultDate' => $data['defaultDate'],
'myReports' => ParentAttendanceReportResource::collection($data['myReports']),
'cutoffThreshold' => $data['cutoffThreshold'],
]);
}
public function submit(ParentAttendanceReportSubmitRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
try {
$result = $this->reportService->submit($parentId, $request->validated());
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json([
'ok' => true,
'inserted' => $result['inserted'],
'blocked' => $result['blocked'],
'students' => $result['students'],
'dates' => $result['dates'],
], 201);
}
public function list(ParentAttendanceReportListRequest $request): JsonResponse
{
$payload = $request->validated();
$start = $payload['start'] ?? now()->toDateString();
$end = $payload['end'] ?? null;
$schoolYear = $payload['school_year'] ?? null;
$semester = $payload['semester'] ?? null;
$rows = $this->reportService->listReports($start, $end, $schoolYear, $semester);
return response()->json([
'ok' => true,
'reports' => ParentAttendanceReportResource::collection($rows),
]);
}
public function checkExisting(ParentAttendanceReportCheckRequest $request): JsonResponse
{
try {
$rows = $this->reportService->checkExisting($request->validated());
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json([
'ok' => true,
'students' => $rows,
]);
}
public function update(ParentAttendanceReportUpdateRequest $request, int $reportId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
try {
$this->reportService->updateReport($parentId, $reportId, $request->validated());
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,284 @@
<?php
namespace App\Http\Controllers\Api\Parents;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Parents\ParentAttendanceRequest;
use App\Http\Requests\Parents\ParentEnrollmentActionRequest;
use App\Http\Requests\Parents\ParentEnrollmentRequest;
use App\Http\Requests\Parents\ParentEmergencyContactRequest;
use App\Http\Requests\Parents\ParentEventParticipationRequest;
use App\Http\Requests\Parents\ParentInvoiceRequest;
use App\Http\Requests\Parents\ParentProfileUpdateRequest;
use App\Http\Requests\Parents\ParentRegistrationRequest;
use App\Http\Requests\Parents\ParentStudentUpdateRequest;
use App\Http\Resources\Parents\ParentAttendanceResource;
use App\Http\Resources\Parents\ParentEmergencyContactResource;
use App\Http\Resources\Parents\ParentStudentResource;
use App\Http\Resources\Invoices\InvoiceResource;
use App\Services\Parents\ParentAttendanceService;
use App\Services\Parents\ParentEmergencyContactService;
use App\Services\Parents\ParentEnrollmentService;
use App\Services\Parents\ParentEventParticipationService;
use App\Services\Parents\ParentInvoiceService;
use App\Services\Parents\ParentProfileService;
use App\Services\Parents\ParentRegistrationService;
use Illuminate\Http\JsonResponse;
class ParentController extends BaseApiController
{
public function __construct(
private ParentAttendanceService $attendanceService,
private ParentInvoiceService $invoiceService,
private ParentEnrollmentService $enrollmentService,
private ParentRegistrationService $registrationService,
private ParentEmergencyContactService $emergencyContactService,
private ParentProfileService $profileService,
private ParentEventParticipationService $eventService
) {
parent::__construct();
}
public function attendance(ParentAttendanceRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->attendanceService->listAttendance($parentId, $request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'attendance' => ParentAttendanceResource::collection($data['attendance']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
]);
}
public function invoices(ParentInvoiceRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$schoolYear = $request->validated()['school_year'] ?? null;
$rows = $this->invoiceService->listInvoices($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($rows),
]);
}
public function enrollments(ParentEnrollmentRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->enrollmentService->overview($parentId, $request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'students' => ParentStudentResource::collection($data['students']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
'withdrawalDeadline' => $data['withdrawalDeadline'],
'lastDayOfRegistration' => $data['lastDayOfRegistration'],
'schoolStartDate' => $data['schoolStartDate'],
]);
}
public function updateEnrollments(ParentEnrollmentActionRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$payload = $request->validated();
$result = $this->enrollmentService->updateEnrollment(
$parentId,
$payload['enroll'] ?? [],
$payload['withdraw'] ?? []
);
return response()->json([
'ok' => true,
'enrolled' => $result['enrolled'],
'withdrawn' => $result['withdrawn'],
]);
}
public function registration(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->registrationService->overview($parentId);
return response()->json([
'ok' => true,
'parent' => $data['parent'],
'existingKids' => ParentStudentResource::collection($data['existingKids']),
'emergencies' => ParentEmergencyContactResource::collection($data['emergencies']),
'maxChilds' => $data['maxChilds'],
'maxEmergency' => $data['maxEmergency'],
'lastDayOfRegistration' => $data['lastDayOfRegistration'],
'registrationAgeDeadline' => $data['registrationAgeDeadline'],
]);
}
public function storeRegistration(ParentRegistrationRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$payload = $request->validated();
$result = $this->registrationService->register(
$parentId,
$payload['students'] ?? [],
$payload['emergency_contacts'] ?? []
);
return response()->json([
'ok' => true,
'created_student_ids' => $result['created_student_ids'],
], 201);
}
public function updateStudent(ParentStudentUpdateRequest $request, int $studentId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$this->registrationService->updateStudent($parentId, $studentId, $request->validated());
return response()->json(['ok' => true]);
}
public function deleteStudent(int $studentId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$this->registrationService->deleteStudent($parentId, $studentId);
return response()->json(['ok' => true]);
}
public function emergencyContacts(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$rows = $this->emergencyContactService->list($parentId);
return response()->json([
'ok' => true,
'contacts' => ParentEmergencyContactResource::collection($rows),
]);
}
public function storeEmergencyContact(ParentEmergencyContactRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$contact = $this->emergencyContactService->store($parentId, $request->validated());
return response()->json([
'ok' => true,
'contact' => new ParentEmergencyContactResource($contact),
], 201);
}
public function updateEmergencyContact(ParentEmergencyContactRequest $request, int $contactId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$contact = $this->emergencyContactService->update($parentId, $contactId, $request->validated());
return response()->json([
'ok' => true,
'contact' => new ParentEmergencyContactResource($contact),
]);
}
public function profile(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$profile = $this->profileService->getProfile($parentId);
return response()->json([
'ok' => true,
'profile' => $profile,
]);
}
public function updateProfile(ParentProfileUpdateRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$profile = $this->profileService->updateProfile($parentId, $request->validated());
return response()->json([
'ok' => true,
'profile' => $profile,
]);
}
public function events(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->eventService->overview($parentId);
return response()->json([
'ok' => true,
'activeEvents' => $data['activeEvents'],
'charges' => $data['charges'],
'yourStudents' => $data['yourStudents'],
]);
}
public function updateParticipation(ParentEventParticipationRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$this->eventService->updateParticipation($parentId, $request->validated()['participation'] ?? []);
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Reports\SlipLogListRequest;
use App\Http\Requests\Reports\SlipPreviewRequest;
use App\Http\Requests\Reports\SlipPrintRequest;
use App\Http\Requests\Reports\SlipReprintRequest;
use App\Http\Resources\Reports\LateSlipLogResource;
use App\Services\Reports\SlipPrinterService;
use Illuminate\Http\JsonResponse;
class SlipPrinterController extends BaseApiController
{
public function __construct(private SlipPrinterService $service)
{
parent::__construct();
}
public function print(SlipPrintRequest $request)
{
$userId = (int) (auth()->id() ?? 0);
$result = $this->service->print($request->validated(), $userId);
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Print failed.'], 422);
}
return response($result['pdf'], 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="' . $result['filename'] . '"',
]);
}
public function preview(SlipPreviewRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
$result = $this->service->preview($request->validated(), $userId);
return response()->json([
'ok' => true,
'text' => $result['text'],
'width' => $result['width'],
]);
}
public function logs(SlipLogListRequest $request): JsonResponse
{
$payload = $request->validated();
$rows = $this->service->logs($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'logs' => LateSlipLogResource::collection($rows),
]);
}
public function reprint(SlipReprintRequest $request)
{
$userId = (int) (auth()->id() ?? 0);
$result = $this->service->reprint((int) $request->validated()['id'], $userId);
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Slip not found.'], 404);
}
return response($result['pdf'], 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="' . $result['filename'] . '"',
]);
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Api\Settings;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Settings\ConfigurationStoreRequest;
use App\Http\Requests\Settings\ConfigurationUpdateRequest;
use App\Http\Resources\Settings\ConfigurationResource;
use App\Services\Settings\ConfigurationService;
use Illuminate\Http\JsonResponse;
class ConfigurationAdminController extends BaseApiController
{
public function __construct(private ConfigurationService $service)
{
parent::__construct();
}
public function index(): JsonResponse
{
$configs = $this->service->list();
return response()->json([
'ok' => true,
'configs' => ConfigurationResource::collection($configs),
]);
}
public function store(ConfigurationStoreRequest $request): JsonResponse
{
$config = $this->service->store($request->validated());
return response()->json([
'ok' => true,
'config' => new ConfigurationResource($config->toArray()),
], 201);
}
public function update(ConfigurationUpdateRequest $request, int $id): JsonResponse
{
$config = $this->service->update($id, $request->validated());
if (!$config) {
return response()->json(['ok' => false, 'message' => 'Configuration not found.'], 404);
}
return response()->json([
'ok' => true,
'config' => new ConfigurationResource($config->toArray()),
]);
}
public function destroy(int $id): JsonResponse
{
$deleted = $this->service->delete($id);
if (!$deleted) {
return response()->json(['ok' => false, 'message' => 'Configuration not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers\Api\Staff;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Teachers\TeacherAbsenceSubmitRequest;
use App\Http\Requests\Teachers\TeacherClassViewRequest;
use App\Http\Requests\Teachers\TeacherSwitchClassRequest;
use App\Http\Resources\Teachers\TeacherAbsenceResource;
use App\Http\Resources\Teachers\TeacherClassResource;
use App\Http\Resources\Teachers\TeacherStudentResource;
use App\Models\User;
use App\Services\Teachers\TeacherAbsenceService;
use App\Services\Teachers\TeacherDashboardService;
use Illuminate\Http\JsonResponse;
class TeacherController extends BaseApiController
{
public function __construct(
private TeacherDashboardService $dashboardService,
private TeacherAbsenceService $absenceService
) {
parent::__construct();
}
public function classes(TeacherClassViewRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
try {
$data = $this->dashboardService->classView(
$userId,
$request->validated()['class_section_id'] ?? null
);
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 403);
}
$teacher = $this->teacherPayload($data['teacher'] ?? null, $userId);
$studentsBySection = [];
foreach (($data['studentsBySection'] ?? []) as $sectionId => $students) {
$studentsBySection[(string) $sectionId] = TeacherStudentResource::collection($students);
}
return response()->json([
'ok' => true,
'teacher' => $teacher,
'classes' => TeacherClassResource::collection($data['classes'] ?? []),
'students' => TeacherStudentResource::collection($data['students'] ?? []),
'students_by_section' => $studentsBySection,
'assigned_names' => $data['assignedNames'] ?? [],
'active_class_section_id' => $data['active_class_section_id'] ?? null,
]);
}
public function switchClass(TeacherSwitchClassRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$requested = (int) $request->validated()['class_section_id'];
try {
$data = $this->dashboardService->classView($userId, $requested);
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 403);
}
if (($data['active_class_section_id'] ?? null) !== $requested) {
return response()->json([
'ok' => false,
'message' => 'You are not assigned to that class.',
], 422);
}
return response()->json([
'ok' => true,
'active_class_section_id' => $requested,
]);
}
public function absenceForm(): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->absenceService->formData($userId);
return response()->json([
'ok' => true,
'teacher_name' => $data['teacher_name'],
'semester' => $data['semester'],
'school_year' => $data['school_year'],
'existing' => TeacherAbsenceResource::collection($data['existing'] ?? []),
'available_dates' => $data['available_dates'] ?? [],
]);
}
public function submitAbsence(TeacherAbsenceSubmitRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$result = $this->absenceService->submit($userId, $request->validated());
return response()->json([
'ok' => (bool) ($result['ok'] ?? false),
'message' => $result['message'] ?? '',
'saved' => $result['saved'] ?? 0,
'dates' => $result['dates'] ?? [],
], (int) ($result['status'] ?? 200));
}
private function teacherPayload(?array $teacher, int $userId): ?array
{
if (!$teacher) {
$model = User::query()->find($userId);
$teacher = $model?->toArray();
}
if (!$teacher) {
return null;
}
return [
'id' => (int) ($teacher['id'] ?? 0),
'firstname' => (string) ($teacher['firstname'] ?? ''),
'lastname' => (string) ($teacher['lastname'] ?? ''),
'email' => $teacher['email'] ?? null,
'role' => User::getUserRoleName($userId),
];
}
}
@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Api\Students;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Students\StudentAssignClassRequest;
use App\Http\Requests\Students\StudentAssignmentListRequest;
use App\Http\Requests\Students\StudentAutoDistributeRequest;
use App\Http\Requests\Students\StudentPromotionTotalsRequest;
use App\Http\Requests\Students\StudentRemoveClassRequest;
use App\Http\Requests\Students\StudentSetActiveRequest;
use App\Http\Requests\Students\StudentUpdateRequest;
use App\Http\Resources\Students\StudentAssignmentResource;
use App\Http\Resources\Students\StudentClassSectionResource;
use App\Http\Resources\Students\StudentRemovedResource;
use App\Http\Resources\Students\StudentScoreCardRowResource;
use App\Services\Students\StudentAssignmentService;
use App\Services\Students\StudentAutoDistributionService;
use App\Services\Students\StudentDirectoryService;
use App\Services\Students\StudentProfileService;
use App\Services\Students\StudentScoreCardService;
use App\Services\Students\StudentStatusService;
use Illuminate\Http\JsonResponse;
class StudentController extends BaseApiController
{
public function __construct(
private StudentAssignmentService $assignmentService,
private StudentDirectoryService $directoryService,
private StudentStatusService $statusService,
private StudentAutoDistributionService $autoDistributionService,
private StudentProfileService $profileService,
private StudentScoreCardService $scoreCardService
) {
parent::__construct();
}
public function assignments(StudentAssignmentListRequest $request): JsonResponse
{
$data = $this->directoryService->assignmentData($request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'students' => StudentAssignmentResource::collection($data['students']),
'classes' => StudentClassSectionResource::collection($data['classes']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
'currentYear' => $data['currentYear'],
'isCurrentYear' => $data['isCurrentYear'],
]);
}
public function assignClass(StudentAssignClassRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->assignmentService->assignClasses(
(int) $payload['student_id'],
$payload['class_section_id'],
(bool) ($payload['is_event_only'] ?? false),
(int) (auth()->id() ?? 0)
);
$status = $result['ok'] ? 200 : 422;
return response()->json($result + ['ok' => (bool) $result['ok']], $status);
}
public function removeClass(StudentRemoveClassRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->assignmentService->removeClass(
(int) $payload['student_id'],
(int) $payload['class_section_id'],
(int) (auth()->id() ?? 0)
);
$status = $result['ok'] ? 200 : 422;
return response()->json($result + ['ok' => (bool) $result['ok']], $status);
}
public function removed(StudentAssignmentListRequest $request): JsonResponse
{
$data = $this->directoryService->removedStudents($request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'active_students' => StudentRemovedResource::collection($data['active_students']),
'removed_students' => StudentRemovedResource::collection($data['removed_students']),
'school_year' => $data['school_year'],
]);
}
public function setActive(StudentSetActiveRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->statusService->setActive(
(int) $payload['student_id'],
(bool) $payload['is_active'],
(int) (auth()->id() ?? 0)
);
$status = $result['ok'] ? 200 : 422;
return response()->json($result + ['ok' => (bool) $result['ok']], $status);
}
public function autoDistribute(StudentAutoDistributeRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->autoDistributionService->autoDistribute(
(int) ($payload['class_id'] ?? 0),
(int) $payload['students_per_section'],
$payload['school_year'] ?? null,
(int) ($payload['class_section_id'] ?? 0),
(int) (auth()->id() ?? 0)
);
$status = $result['ok'] ? 200 : 422;
return response()->json($result + ['ok' => (bool) $result['ok']], $status);
}
public function promotionTotals(StudentPromotionTotalsRequest $request): JsonResponse
{
$data = $this->autoDistributionService->promotionTotals($request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'year' => $data['year'],
'rows' => $data['rows'],
]);
}
public function update(StudentUpdateRequest $request, int $studentId): JsonResponse
{
$result = $this->profileService->updateStudent($studentId, $request->validated());
$status = $result['ok'] ? 200 : 422;
return response()->json($result + ['ok' => (bool) $result['ok']], $status);
}
public function scoreCard(int $studentId): JsonResponse
{
$result = $this->scoreCardService->scoreCard($studentId);
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Not found.'], 404);
}
return response()->json([
'ok' => true,
'student' => $result['student'],
'rows' => StudentScoreCardRowResource::collection($result['rows']),
]);
}
}
@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api\Subjects;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Subjects\SubjectCurriculumStoreRequest;
use App\Http\Requests\Subjects\SubjectCurriculumUpdateRequest;
use App\Http\Resources\Subjects\SubjectClassResource;
use App\Http\Resources\Subjects\SubjectCurriculumResource;
use App\Models\SubjectCurriculum;
use App\Services\Subjects\SubjectCurriculumService;
use Illuminate\Http\JsonResponse;
class SubjectCurriculumController extends BaseApiController
{
public function __construct(private SubjectCurriculumService $service)
{
parent::__construct();
}
public function index(): JsonResponse
{
$data = $this->service->list();
return response()->json([
'ok' => true,
'classes' => SubjectClassResource::collection($data['classes']),
'entries' => SubjectCurriculumResource::collection($data['entries']),
'subject_labels' => $data['subject_labels'],
]);
}
public function show(int $id): JsonResponse
{
$entry = SubjectCurriculum::query()->find($id);
if (!$entry) {
return response()->json(['ok' => false, 'message' => 'Curriculum entry not found.'], 404);
}
return response()->json([
'ok' => true,
'entry' => new SubjectCurriculumResource($entry->toArray()),
]);
}
public function store(SubjectCurriculumStoreRequest $request): JsonResponse
{
$entry = $this->service->store($request->validated());
return response()->json([
'ok' => true,
'entry' => new SubjectCurriculumResource($entry->toArray()),
], 201);
}
public function update(SubjectCurriculumUpdateRequest $request, int $id): JsonResponse
{
$entry = $this->service->update($id, $request->validated());
if (!$entry) {
return response()->json(['ok' => false, 'message' => 'Curriculum entry not found.'], 404);
}
return response()->json([
'ok' => true,
'entry' => new SubjectCurriculumResource($entry->toArray()),
]);
}
public function destroy(int $id): JsonResponse
{
$deleted = $this->service->delete($id);
if (!$deleted) {
return response()->json(['ok' => false, 'message' => 'Curriculum entry not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api\System;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\SchoolIds\SchoolIdAssignRequest;
use App\Http\Resources\SchoolIds\SchoolIdResource;
use App\Services\SchoolIds\SchoolIdAssignmentService;
use App\Services\SchoolIds\SchoolIdGenerationService;
use Illuminate\Http\JsonResponse;
class SchoolIdController extends BaseApiController
{
public function __construct(
private SchoolIdAssignmentService $assignmentService,
private SchoolIdGenerationService $generationService
) {
parent::__construct();
}
public function assign(SchoolIdAssignRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolId = $this->assignmentService->assignToUser((int) $payload['user_id']);
if ($schoolId === null) {
return response()->json(['ok' => false, 'message' => 'User not found.'], 404);
}
return response()->json([
'ok' => true,
'result' => new SchoolIdResource([
'type' => 'user',
'school_id' => $schoolId,
]),
]);
}
public function generateStudent(): JsonResponse
{
$schoolId = $this->generationService->generateStudentId();
return response()->json([
'ok' => true,
'result' => new SchoolIdResource([
'type' => 'student',
'school_id' => $schoolId,
]),
]);
}
public function generateUser(): JsonResponse
{
$schoolId = $this->generationService->generateUserId();
return response()->json([
'ok' => true,
'result' => new SchoolIdResource([
'type' => 'user',
'school_id' => $schoolId,
]),
]);
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\EmergencyContacts;
use App\Http\Requests\ApiFormRequest;
class EmergencyContactUpdateRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:150'],
'cellphone' => ['required', 'string', 'max:30'],
'email' => ['nullable', 'email', 'max:150'],
'relation' => ['nullable', 'string', 'max:80'],
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\Exams;
use App\Http\Requests\ApiFormRequest;
class ExamDraftAdminLegacyRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'class_section_id' => ['required', 'integer', 'min:1'],
'school_year' => ['required', 'string'],
'semester' => ['required', 'string'],
'exam_type' => ['nullable', 'string', 'max:50'],
'old_exam_file' => ['required', 'file', 'mimes:doc,docx,pdf', 'max:12288'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Exams;
use App\Http\Requests\ApiFormRequest;
class ExamDraftAdminReviewRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'draft_id' => ['required', 'integer', 'min:1'],
'review_status' => ['nullable', 'in:draft,submitted,reviewed,finalized,rejected,pending,final,approved'],
'admin_comments' => ['nullable', 'string'],
'final_file' => ['nullable', 'file', 'mimes:doc,docx,pdf', 'max:12288'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Exams;
use App\Http\Requests\ApiFormRequest;
class ExamDraftTeacherStoreRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'class_section_id' => ['required', 'integer', 'min:1'],
'exam_type' => ['nullable', 'string', 'max:50'],
'description' => ['nullable', 'string'],
'draft_file' => ['nullable', 'file', 'mimes:doc,docx', 'max:12288'],
];
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentAttendanceReportCheckRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'date' => ['nullable', 'date'],
'dates' => ['nullable', 'array'],
'dates.*' => ['date'],
'type' => ['required', 'in:absent,late,early_dismissal'],
'student_ids' => ['required', 'array'],
'student_ids.*' => ['integer', 'min:1'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentAttendanceReportListRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'start' => ['nullable', 'date'],
'end' => ['nullable', 'date'],
'school_year' => ['nullable', 'string', 'max:20'],
'semester' => ['nullable', 'string', 'max:20'],
];
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentAttendanceReportSubmitRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'student_ids' => ['required', 'array'],
'student_ids.*' => ['integer', 'min:1'],
'date' => ['nullable', 'date'],
'dates' => ['nullable', 'array'],
'dates.*' => ['date'],
'type' => ['required', 'in:absent,late,early_dismissal'],
'arrival_time' => ['nullable', 'string'],
'dismiss_time' => ['nullable', 'string'],
'reason' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentAttendanceReportUpdateRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'reason' => ['nullable', 'string'],
'arrival_time' => ['nullable', 'string'],
'dismiss_time' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentAttendanceRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string', 'max:20'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentEmergencyContactRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:150'],
'cellphone' => ['required', 'string', 'max:30'],
'email' => ['nullable', 'email', 'max:150'],
'relation' => ['nullable', 'string', 'max:80'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentEnrollmentActionRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'enroll' => ['nullable', 'array'],
'enroll.*' => ['integer', 'min:1'],
'withdraw' => ['nullable', 'array'],
'withdraw.*' => ['integer', 'min:1'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentEnrollmentRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string', 'max:20'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentEventParticipationRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'participation' => ['required', 'array'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentInvoiceRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string', 'max:20'],
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentProfileUpdateRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'firstname' => ['required', 'string', 'max:255'],
'lastname' => ['required', 'string', 'max:255'],
'cellphone' => ['required', 'string', 'max:25'],
'address_street' => ['required', 'string', 'max:255'],
'city' => ['required', 'string', 'max:255'],
'state' => ['required', 'string', 'max:25'],
'zip' => ['required', 'string', 'max:25'],
];
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentRegistrationRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'students' => ['nullable', 'array'],
'students.*.firstname' => ['required_with:students', 'string', 'max:255'],
'students.*.lastname' => ['required_with:students', 'string', 'max:255'],
'students.*.dob' => ['required_with:students', 'date'],
'students.*.gender' => ['required_with:students', 'string', 'max:10'],
'students.*.registration_grade' => ['nullable', 'string', 'max:50'],
'students.*.photo_consent' => ['nullable', 'boolean'],
'students.*.is_new' => ['nullable', 'boolean'],
'students.*.allergies' => ['nullable', 'array'],
'students.*.medical_conditions' => ['nullable', 'array'],
'emergency_contacts' => ['nullable', 'array'],
'emergency_contacts.*.name' => ['required_with:emergency_contacts', 'string', 'max:150'],
'emergency_contacts.*.cellphone' => ['required_with:emergency_contacts', 'string', 'max:30'],
'emergency_contacts.*.email' => ['nullable', 'email', 'max:150'],
'emergency_contacts.*.relation' => ['nullable', 'string', 'max:80'],
];
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Parents;
use App\Http\Requests\ApiFormRequest;
class ParentStudentUpdateRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'firstname' => ['required', 'string', 'max:255'],
'lastname' => ['required', 'string', 'max:255'],
'dob' => ['required', 'date'],
'gender' => ['required', 'string', 'max:10'],
'photo_consent' => ['nullable', 'boolean'],
'registration_grade' => ['nullable', 'string', 'max:50'],
'allergies' => ['nullable', 'array'],
'medical_conditions' => ['nullable', 'array'],
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Http\Requests\Reports;
use App\Http\Requests\ApiFormRequest;
class SlipLogListRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string'],
'semester' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Reports;
use App\Http\Requests\ApiFormRequest;
class SlipPreviewRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string'],
'student_name' => ['nullable', 'string'],
'date' => ['nullable', 'string'],
'time_in' => ['nullable', 'string'],
'grade' => ['nullable', 'string'],
'reason' => ['nullable', 'string'],
'admin_name' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Reports;
use App\Http\Requests\ApiFormRequest;
class SlipPrintRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string'],
'student_name' => ['required', 'string'],
'date' => ['nullable', 'string'],
'time_in' => ['nullable', 'string'],
'grade' => ['nullable', 'string'],
'reason' => ['nullable', 'string'],
'admin_name' => ['nullable', 'string'],
'paper' => ['nullable', 'string'],
'font_pt' => ['nullable', 'numeric'],
'line_h' => ['nullable', 'numeric'],
'rows' => ['nullable', 'integer'],
'height_mm' => ['nullable', 'numeric'],
'h' => ['nullable', 'numeric'],
'fudge_mm' => ['nullable', 'numeric'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Reports;
use App\Http\Requests\ApiFormRequest;
class SlipReprintRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'id' => ['required', 'integer', 'min:1'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\SchoolIds;
use App\Http\Requests\ApiFormRequest;
class SchoolIdAssignRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'user_id' => ['required', 'integer', 'min:1', 'exists:users,id'],
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Http\Requests\Settings;
use App\Http\Requests\ApiFormRequest;
class ConfigurationStoreRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'config_key' => ['required', 'string', 'max:255'],
'config_value' => ['required', 'string'],
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Http\Requests\Settings;
use App\Http\Requests\ApiFormRequest;
class ConfigurationUpdateRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'config_key' => ['required', 'string', 'max:255'],
'config_value' => ['required', 'string'],
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Students;
use App\Http\Requests\ApiFormRequest;
class StudentAssignClassRequest extends ApiFormRequest
{
protected function prepareForValidation(): void
{
$value = $this->input('class_section_id');
if (is_string($value)) {
$parts = preg_split('/[,\s]+/', $value, -1, PREG_SPLIT_NO_EMPTY) ?: [];
$this->merge(['class_section_id' => $parts]);
}
}
public function rules(): array
{
return [
'student_id' => ['required', 'integer', 'min:1'],
'class_section_id' => ['required', 'array', 'min:1'],
'class_section_id.*' => ['integer', 'min:1'],
'is_event_only' => ['nullable', 'boolean'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Students;
use App\Http\Requests\ApiFormRequest;
class StudentAssignmentListRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Students;
use App\Http\Requests\ApiFormRequest;
class StudentAutoDistributeRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'class_id' => ['nullable', 'integer', 'min:1'],
'class_section_id' => ['nullable', 'integer', 'min:1'],
'students_per_section' => ['required', 'integer', 'min:1'],
'school_year' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Students;
use App\Http\Requests\ApiFormRequest;
class StudentPromotionTotalsRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Http\Requests\Students;
use App\Http\Requests\ApiFormRequest;
class StudentRemoveClassRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'student_id' => ['required', 'integer', 'min:1'],
'class_section_id' => ['required', 'integer', 'min:1'],
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Http\Requests\Students;
use App\Http\Requests\ApiFormRequest;
class StudentSetActiveRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'student_id' => ['required', 'integer', 'min:1'],
'is_active' => ['required', 'boolean'],
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Students;
use App\Http\Requests\ApiFormRequest;
class StudentUpdateRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_id' => ['nullable', 'string', 'max:100'],
'firstname' => ['required', 'string', 'max:255'],
'lastname' => ['required', 'string', 'max:255'],
'dob' => ['required', 'date'],
'age' => ['nullable', 'integer'],
'gender' => ['required', 'in:Male,Female,Other'],
'registration_grade' => ['required', 'string', 'max:50'],
'photo_consent' => ['nullable', 'boolean'],
'parent_id' => ['required', 'integer', 'min:1'],
'registration_date' => ['nullable', 'date'],
'tuition_paid' => ['nullable', 'boolean'],
'year_of_registration' => ['nullable', 'string'],
'school_year' => ['nullable', 'string'],
'rfid_tag' => ['nullable', 'string', 'max:100'],
'semester' => ['nullable', 'string'],
'is_new' => ['required', 'boolean'],
'is_active' => ['nullable', 'boolean'],
'medical_conditions' => ['nullable', 'string'],
'allergies' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\Subjects;
use App\Http\Requests\ApiFormRequest;
class SubjectCurriculumStoreRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'class_id' => ['required', 'integer', 'min:1'],
'subject' => ['required', 'in:islamic,quran'],
'chapter_name' => ['required', 'string', 'max:255'],
'unit_number' => ['nullable', 'integer'],
'unit_title' => ['nullable', 'string', 'max:255'],
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Requests\Subjects;
use App\Http\Requests\ApiFormRequest;
class SubjectCurriculumUpdateRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'class_id' => ['required', 'integer', 'min:1'],
'subject' => ['required', 'in:islamic,quran'],
'chapter_name' => ['required', 'string', 'max:255'],
'unit_number' => ['nullable', 'integer'],
'unit_title' => ['nullable', 'string', 'max:255'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Teachers;
use App\Http\Requests\ApiFormRequest;
class TeacherAbsenceSubmitRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'dates' => ['required', 'array', 'min:1'],
'dates.*' => ['date'],
'reason' => ['required', 'string'],
'reason_type' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Teachers;
use App\Http\Requests\ApiFormRequest;
class TeacherAssignmentDeleteRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'teacher_id' => ['required', 'integer', 'min:1'],
'class_section_id' => ['required', 'integer', 'min:1'],
'position' => ['required', 'in:main,ta'],
'school_year' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Teachers;
use App\Http\Requests\ApiFormRequest;
class TeacherAssignmentListRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'school_year' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Teachers;
use App\Http\Requests\ApiFormRequest;
class TeacherAssignmentStoreRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'teacher_id' => ['required', 'integer', 'min:1'],
'class_section_id' => ['required', 'integer', 'min:1'],
'teacher_role' => ['nullable', 'string'],
'school_year' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Teachers;
use App\Http\Requests\ApiFormRequest;
class TeacherClassViewRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'class_section_id' => ['nullable', 'integer', 'min:1'],
];
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Http\Requests\Teachers;
use App\Http\Requests\ApiFormRequest;
class TeacherSwitchClassRequest extends ApiFormRequest
{
public function rules(): array
{
return [
'class_section_id' => ['required', 'integer', 'min:1'],
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\EmergencyContacts;
use Illuminate\Http\Resources\Json\JsonResource;
class EmergencyContactGroupResource extends JsonResource
{
public function toArray($request): array
{
return [
'parent_id' => (int) ($this['parent_id'] ?? 0),
'parent_name' => (string) ($this['parent_name'] ?? ''),
'parent_phones' => array_values($this['parent_phones'] ?? []),
'students' => EmergencyContactStudentResource::collection($this['students'] ?? []),
'contacts' => EmergencyContactResource::collection($this['contacts'] ?? []),
];
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources\EmergencyContacts;
use Illuminate\Http\Resources\Json\JsonResource;
class EmergencyContactResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'parent_id' => (int) ($this['parent_id'] ?? 0),
'name' => (string) ($this['emergency_contact_name'] ?? ''),
'cellphone' => (string) ($this['cellphone'] ?? ''),
'email' => $this['email'] ?? null,
'relation' => $this['relation'] ?? null,
'semester' => $this['semester'] ?? null,
'school_year' => $this['school_year'] ?? null,
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Resources\EmergencyContacts;
use Illuminate\Http\Resources\Json\JsonResource;
class EmergencyContactStudentResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'firstname' => (string) ($this['firstname'] ?? ''),
'lastname' => (string) ($this['lastname'] ?? ''),
'school_id' => $this['school_id'] ?? null,
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Resources\Exams;
use Illuminate\Http\Resources\Json\JsonResource;
class ExamDraftResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'teacher_id' => (int) ($this['teacher_id'] ?? 0),
'class_section_id' => (int) ($this['class_section_id'] ?? 0),
'class_section_name' => $this['class_section_name'] ?? null,
'semester' => $this['semester'] ?? null,
'school_year' => $this['school_year'] ?? null,
'exam_type' => $this['exam_type'] ?? null,
'draft_title' => $this['draft_title'] ?? null,
'description' => $this['description'] ?? null,
'teacher_file' => $this['teacher_file'] ?? null,
'teacher_filename' => $this['teacher_filename'] ?? null,
'status' => $this['status'] ?? null,
'admin_id' => $this['admin_id'] ?? null,
'admin_comments' => $this['admin_comments'] ?? null,
'reviewed_at' => $this['reviewed_at'] ?? null,
'final_file' => $this['final_file'] ?? null,
'final_filename' => $this['final_filename'] ?? null,
'final_pdf_file' => $this['final_pdf_file'] ?? null,
'version' => $this['version'] ?? null,
'previous_draft_id' => $this['previous_draft_id'] ?? null,
'is_legacy' => $this['is_legacy'] ?? null,
'teacher_first' => $this['teacher_first'] ?? null,
'teacher_last' => $this['teacher_last'] ?? null,
'admin_first' => $this['admin_first'] ?? null,
'admin_last' => $this['admin_last'] ?? null,
];
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources\Parents;
use Illuminate\Http\Resources\Json\JsonResource;
class ParentAttendanceReportResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'parent_id' => (int) ($this['parent_id'] ?? 0),
'student_id' => (int) ($this['student_id'] ?? 0),
'class_section_id' => $this['class_section_id'] ?? null,
'report_date' => (string) ($this['report_date'] ?? ''),
'type' => (string) ($this['type'] ?? ''),
'arrival_time' => $this['arrival_time'] ?? null,
'dismiss_time' => $this['dismiss_time'] ?? null,
'reason' => $this['reason'] ?? null,
'semester' => $this['semester'] ?? null,
'school_year' => $this['school_year'] ?? null,
'status' => $this['status'] ?? null,
'firstname' => $this['firstname'] ?? null,
'lastname' => $this['lastname'] ?? null,
'class_section_name' => $this['class_section_name'] ?? null,
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Parents;
use Illuminate\Http\Resources\Json\JsonResource;
class ParentAttendanceResource extends JsonResource
{
public function toArray($request): array
{
return [
'firstname' => (string) ($this['firstname'] ?? ''),
'lastname' => (string) ($this['lastname'] ?? ''),
'date' => (string) ($this['date'] ?? ''),
'status' => (string) ($this['status'] ?? ''),
'reason' => $this['reason'] ?? null,
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources\Parents;
use Illuminate\Http\Resources\Json\JsonResource;
class ParentEmergencyContactResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'name' => (string) ($this['emergency_contact_name'] ?? ''),
'cellphone' => (string) ($this['cellphone'] ?? ''),
'email' => $this['email'] ?? null,
'relation' => $this['relation'] ?? null,
'semester' => $this['semester'] ?? null,
'school_year' => $this['school_year'] ?? null,
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources\Parents;
use Illuminate\Http\Resources\Json\JsonResource;
class ParentStudentResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'firstname' => (string) ($this['firstname'] ?? ''),
'lastname' => (string) ($this['lastname'] ?? ''),
'dob' => $this['dob'] ?? null,
'gender' => $this['gender'] ?? null,
'registration_grade' => $this['registration_grade'] ?? null,
'school_year' => $this['school_year'] ?? null,
'semester' => $this['semester'] ?? null,
'class_section' => $this['class_section'] ?? null,
'enrollment_status' => $this['enrollment_status'] ?? null,
'admission_status' => $this['admission_status'] ?? null,
'disable_enroll' => (bool) ($this['disable_enroll'] ?? false),
'allergies' => $this['allergies'] ?? [],
'medical_conditions' => $this['medical_conditions'] ?? [],
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources\Reports;
use Illuminate\Http\Resources\Json\JsonResource;
class LateSlipLogResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'school_year' => (string) ($this['school_year'] ?? ''),
'semester' => (string) ($this['semester'] ?? ''),
'student_name' => (string) ($this['student_name'] ?? ''),
'date' => $this['date'] ?? null,
'time_in' => $this['time_in'] ?? null,
'grade' => $this['grade'] ?? null,
'reason' => $this['reason'] ?? null,
'admin_name' => $this['admin_name'] ?? null,
'printed_at' => $this['printed_at'] ?? null,
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Http\Resources\SchoolIds;
use Illuminate\Http\Resources\Json\JsonResource;
class SchoolIdResource extends JsonResource
{
public function toArray($request): array
{
return [
'type' => $this['type'] ?? null,
'school_id' => $this['school_id'] ?? null,
];
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Resources\Settings;
use Illuminate\Http\Resources\Json\JsonResource;
class ConfigurationResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'config_key' => (string) ($this['config_key'] ?? ''),
'config_value' => (string) ($this['config_value'] ?? ''),
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources\Students;
use Illuminate\Http\Resources\Json\JsonResource;
class StudentAssignmentResource extends JsonResource
{
public function toArray($request): array
{
return [
'student_id' => (int) ($this['student_id'] ?? 0),
'name' => (string) ($this['name'] ?? ''),
'age' => $this['age'] ?? null,
'email' => $this['email'] ?? null,
'phone' => $this['phone'] ?? null,
'registration_grade' => $this['registration_grade'] ?? null,
'class_section_name' => $this['class_section_name'] ?? null,
'class_section_names' => $this['class_section_names'] ?? [],
'class_section_ids' => $this['class_section_ids'] ?? [],
'new_student' => $this['new_student'] ?? null,
'registration_date' => $this['registration_date'] ?? null,
'school_year' => $this['school_year'] ?? null,
'semester' => $this['semester'] ?? null,
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Resources\Students;
use Illuminate\Http\Resources\Json\JsonResource;
class StudentClassSectionResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'class_section_id' => (int) ($this['class_section_id'] ?? 0),
'class_section_name' => (string) ($this['class_section_name'] ?? ''),
'school_year' => $this['school_year'] ?? null,
];
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources\Students;
use Illuminate\Http\Resources\Json\JsonResource;
class StudentRemovedResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'school_id' => $this['school_id'] ?? null,
'firstname' => $this['firstname'] ?? null,
'lastname' => $this['lastname'] ?? null,
'gender' => $this['gender'] ?? null,
'age' => $this['age'] ?? null,
'class_sections' => $this['class_sections'] ?? [],
'class_section_name' => $this['class_section_name'] ?? null,
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources\Students;
use Illuminate\Http\Resources\Json\JsonResource;
class StudentScoreCardRowResource extends JsonResource
{
public function toArray($request): array
{
return [
'school_year' => $this['school_year'] ?? null,
'semester' => $this['semester'] ?? null,
'homework_avg' => $this['homework_avg'] ?? null,
'project_avg' => $this['project_avg'] ?? null,
'participation_score' => $this['participation_score'] ?? null,
'quiz_avg' => $this['quiz_avg'] ?? null,
'test_avg' => $this['test_avg'] ?? null,
'attendance_score' => $this['attendance_score'] ?? null,
'ptap_score' => $this['ptap_score'] ?? null,
'midterm_exam_score' => $this['midterm_exam_score'] ?? null,
'semester_score' => $this['semester_score'] ?? null,
'class_section_name' => $this['class_section_name'] ?? null,
'comments' => $this['comments'] ?? [],
'year_score' => $this['year_score'] ?? null,
];
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Resources\Subjects;
use Illuminate\Http\Resources\Json\JsonResource;
class SubjectClassResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'class_name' => (string) ($this['class_name'] ?? ''),
'school_year' => $this['school_year'] ?? null,
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources\Subjects;
use Illuminate\Http\Resources\Json\JsonResource;
class SubjectCurriculumResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'class_id' => (int) ($this['class_id'] ?? 0),
'class_name' => $this['class_name'] ?? null,
'subject' => (string) ($this['subject'] ?? ''),
'unit_number' => $this['unit_number'] ?? null,
'unit_title' => $this['unit_title'] ?? null,
'chapter_name' => (string) ($this['chapter_name'] ?? ''),
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources\Teachers;
use Illuminate\Http\Resources\Json\JsonResource;
class TeacherAbsenceResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => (int) ($this['id'] ?? 0),
'user_id' => (int) ($this['user_id'] ?? 0),
'date' => (string) ($this['date'] ?? ''),
'semester' => $this['semester'] ?? null,
'school_year' => $this['school_year'] ?? null,
'status' => $this['status'] ?? null,
'reason' => $this['reason'] ?? null,
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources\Teachers;
use Illuminate\Http\Resources\Json\JsonResource;
class TeacherAssignmentResource extends JsonResource
{
public function toArray($request): array
{
return [
'teacher_id' => (int) ($this['teacher_id'] ?? 0),
'firstname' => (string) ($this['firstname'] ?? ''),
'lastname' => (string) ($this['lastname'] ?? ''),
'name' => (string) ($this['name'] ?? ''),
'email' => $this['email'] ?? null,
'cellphone' => $this['cellphone'] ?? null,
'role' => $this['role'] ?? null,
'school_year' => $this['school_year'] ?? null,
'main_assignments' => $this['main_assignments'] ?? [],
'ta_assignments' => $this['ta_assignments'] ?? [],
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources\Teachers;
use Illuminate\Http\Resources\Json\JsonResource;
class TeacherClassResource extends JsonResource
{
public function toArray($request): array
{
return [
'class_section_id' => (int) ($this['class_section_id'] ?? 0),
'class_section_name' => (string) ($this['class_section_name'] ?? ''),
'class_id' => $this['class_id'] ?? null,
'class_name' => $this['class_name'] ?? null,
'teacher_role' => $this['teacher_role'] ?? null,
'school_year' => $this['school_year'] ?? null,
'semester' => $this['semester'] ?? null,
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources\Teachers;
use Illuminate\Http\Resources\Json\JsonResource;
class TeacherStudentResource extends JsonResource
{
public function toArray($request): array
{
return [
'student_id' => (int) ($this['student_id'] ?? 0),
'firstname' => (string) ($this['firstname'] ?? ''),
'lastname' => (string) ($this['lastname'] ?? ''),
'school_id' => $this['school_id'] ?? null,
'is_new' => $this['is_new'] ?? null,
'photo_consent' => $this['photo_consent'] ?? null,
'age' => $this['age'] ?? null,
'school_year' => $this['school_year'] ?? null,
'class_section_id' => $this['class_section_id'] ?? null,
'medical_conditions' => $this['medical_conditions'] ?? [],
'allergies' => $this['allergies'] ?? [],
'medical_conditions_text' => $this['medical_conditions_text'] ?? '',
'allergies_text' => $this['allergies_text'] ?? '',
];
}
}
+2 -1
View File
@@ -27,6 +27,7 @@ class ExamDraft extends BaseModel
'reviewed_at', 'reviewed_at',
'final_file', 'final_file',
'final_filename', 'final_filename',
'final_pdf_file',
'version', 'version',
'previous_draft_id', 'previous_draft_id',
'is_legacy', 'is_legacy',
@@ -87,4 +88,4 @@ class ExamDraft extends BaseModel
{ {
return $this->belongsTo(self::class, 'previous_draft_id'); return $this->belongsTo(self::class, 'previous_draft_id');
} }
} }
@@ -0,0 +1,34 @@
<?php
namespace App\Services\EmergencyContacts;
use App\Models\EmergencyContact;
use App\Services\PhoneFormatterService;
class EmergencyContactCrudService
{
public function __construct(
private PhoneFormatterService $phoneFormatter
) {
}
public function update(int $contactId, array $payload): array
{
$contact = EmergencyContact::query()->findOrFail($contactId);
$contact->update([
'emergency_contact_name' => $payload['name'],
'cellphone' => $this->phoneFormatter->formatPhoneNumber($payload['cellphone']),
'email' => $payload['email'] ?? null,
'relation' => $payload['relation'] ?? null,
]);
return $contact->toArray();
}
public function delete(int $contactId): void
{
$contact = EmergencyContact::query()->findOrFail($contactId);
$contact->delete();
}
}
@@ -0,0 +1,87 @@
<?php
namespace App\Services\EmergencyContacts;
use App\Models\EmergencyContact;
use App\Models\Student;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
class EmergencyContactDirectoryService
{
public function groups(): array
{
$parentIds = EmergencyContact::query()
->distinct()
->pluck('parent_id')
->filter(static fn ($id) => (int) $id > 0)
->values()
->all();
$groups = [];
foreach ($parentIds as $parentId) {
$parentId = (int) $parentId;
$parent = User::query()->find($parentId);
$parentName = $parent
? trim((string) $parent->firstname . ' ' . (string) $parent->lastname)
: 'Unknown Parent';
if ($parentName === '') {
$parentName = 'Unknown Parent';
}
$parentPhone = $parent ? (string) ($parent->cellphone ?? '') : '';
$secondPhone = $this->secondParentPhone($parentId);
$students = Student::query()
->select(['id', 'firstname', 'lastname', 'school_id'])
->where('parent_id', $parentId)
->get()
->toArray();
$contacts = EmergencyContact::query()
->where('parent_id', $parentId)
->orderBy('updated_at', 'DESC')
->get()
->toArray();
$groups[] = [
'parent_id' => $parentId,
'parent_name' => $parentName,
'parent_phones' => array_values(array_filter([
$parentPhone,
$secondPhone,
], static fn ($value) => (string) $value !== '')),
'students' => $students,
'contacts' => $contacts,
];
}
return $groups;
}
private function secondParentPhone(int $parentId): string
{
try {
if (!Schema::hasTable('parents')) {
return '';
}
if (!Schema::hasColumn('parents', 'secondparent_phone')) {
return '';
}
$phone = DB::table('parents')
->where('firstparent_id', $parentId)
->orderByDesc('updated_at')
->value('secondparent_phone');
return $phone ? (string) $phone : '';
} catch (\Throwable $e) {
Log::debug('EmergencyContactDirectoryService: could not load second parent phone: ' . $e->getMessage());
return '';
}
}
}
@@ -0,0 +1,225 @@
<?php
namespace App\Services\Exams;
use App\Models\ClassSection;
use App\Models\ExamDraft;
use Illuminate\Support\Facades\DB;
class ExamDraftAdminService
{
public function __construct(
private ExamDraftConfigService $configService,
private ExamDraftFileService $fileService
) {
}
public function list(): array
{
$context = $this->configService->context();
$drafts = DB::table('exam_drafts as ed')
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ed.class_section_id')
->leftJoin('users as u', 'u.id', '=', 'ed.teacher_id')
->leftJoin('users as a', 'a.id', '=', 'ed.admin_id')
->select(
'ed.*',
'cs.class_section_name',
'u.firstname as teacher_first',
'u.lastname as teacher_last',
'a.firstname as admin_first',
'a.lastname as admin_last'
)
->orderByDesc('ed.created_at')
->get()
->map(fn ($r) => (array) $r)
->all();
$classSections = ClassSection::query()
->select('class_section_id', 'class_section_name')
->orderBy('class_section_name')
->get()
->toArray();
$legacyByClass = [];
if ($context['has_legacy'] ?? false) {
foreach ($drafts as $row) {
$isLegacy = !empty($row['is_legacy']);
if (!$isLegacy) {
continue;
}
$cid = (int) ($row['class_section_id'] ?? 0);
if (!isset($legacyByClass[$cid])) {
$legacyByClass[$cid] = [
'class_section_id' => $cid,
'class_section_name' => $row['class_section_name'] ?? 'Class ' . $cid,
'items' => [],
];
}
$legacyByClass[$cid]['items'][] = $row;
}
}
return [
'drafts' => $drafts,
'classSections' => $classSections,
'legacyByClass' => $legacyByClass,
'examTypes' => ExamDraftConfigService::EXAM_TYPES,
'statusOptions' => ExamDraftConfigService::STATUS_OPTIONS,
'schoolYear' => (string) ($context['school_year'] ?? ''),
'semester' => (string) ($context['semester'] ?? ''),
'allowedExtensions' => ExamDraftConfigService::ADMIN_ALLOWED_EXTENSIONS,
'maxUploadBytes' => ExamDraftConfigService::MAX_UPLOAD_BYTES,
];
}
public function uploadLegacy(int $adminId, array $payload, \Illuminate\Http\UploadedFile $file): array
{
$context = $this->configService->context();
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
$schoolYear = trim((string) ($payload['school_year'] ?? $context['school_year'] ?? ''));
$semester = trim((string) ($payload['semester'] ?? $context['semester'] ?? ''));
$examType = trim((string) ($payload['exam_type'] ?? ''));
if ($classSectionId <= 0 || $schoolYear === '' || $semester === '') {
return ['ok' => false, 'message' => 'Select class section, school year, and semester.'];
}
$stored = $this->fileService->storeUploadedFile(
$file,
ExamDraftConfigService::FINAL_UPLOAD_DIR,
ExamDraftConfigService::ADMIN_ALLOWED_EXTENSIONS,
ExamDraftConfigService::MAX_UPLOAD_BYTES
);
if ($stored === null) {
return ['ok' => false, 'message' => 'File type not allowed or upload failed.'];
}
$payload = [
'teacher_id' => $adminId,
'class_section_id' => $classSectionId,
'semester' => ucfirst(strtolower($semester)),
'school_year' => $schoolYear,
'exam_type' => $examType !== '' ? $examType : null,
'draft_title' => $examType !== '' ? $examType : 'Legacy Exam',
'final_file' => $stored['stored'],
'final_filename' => $stored['original'],
'status' => 'finalized',
'admin_id' => $adminId,
'reviewed_at' => now(),
'version' => 1,
];
if ($context['has_legacy'] ?? false) {
$payload['is_legacy'] = 1;
}
$pdfName = null;
if ($stored['extension'] === 'pdf') {
$pdfName = $stored['stored'];
} else {
$pdfName = $this->fileService->ensurePdfExists(
$stored['stored'],
$stored['extension'],
ExamDraftConfigService::FINAL_UPLOAD_DIR
);
}
if ($pdfName !== null && ($context['has_final_pdf'] ?? false)) {
$payload['final_pdf_file'] = $pdfName;
}
$draft = ExamDraft::query()->create($payload);
return ['ok' => true, 'draft' => $draft->toArray()];
}
public function review(int $adminId, array $payload, ?\Illuminate\Http\UploadedFile $file): array
{
$draftId = (int) ($payload['draft_id'] ?? 0);
if ($draftId <= 0) {
return ['ok' => false, 'message' => 'Invalid submission selected.'];
}
$draft = ExamDraft::query()->find($draftId);
if (!$draft) {
return ['ok' => false, 'message' => 'Submission not found.'];
}
$comments = trim((string) ($payload['admin_comments'] ?? ''));
$statusInput = trim((string) ($payload['review_status'] ?? ''));
$currentStatus = (string) ($draft->status ?? 'draft');
$status = $this->normalizeStatus($statusInput !== '' ? $statusInput : $currentStatus, $currentStatus);
if (!in_array($status, ExamDraftConfigService::STATUS_OPTIONS, true)) {
$status = 'reviewed';
}
$update = [
'status' => $status,
'admin_comments' => $comments !== '' ? $comments : null,
'admin_id' => $adminId,
'reviewed_at' => now(),
];
if ($file) {
$stored = $this->fileService->storeUploadedFile(
$file,
ExamDraftConfigService::FINAL_UPLOAD_DIR,
ExamDraftConfigService::ADMIN_ALLOWED_EXTENSIONS,
ExamDraftConfigService::MAX_UPLOAD_BYTES
);
if ($stored === null) {
return ['ok' => false, 'message' => 'Unable to store the final draft.'];
}
$update['final_file'] = $stored['stored'];
$update['final_filename'] = $stored['original'];
$pdfName = $this->fileService->ensurePdfExists(
$stored['stored'],
$stored['extension'],
ExamDraftConfigService::FINAL_UPLOAD_DIR
);
if ($pdfName !== null && ($this->configService->context()['has_final_pdf'] ?? false)) {
$update['final_pdf_file'] = $pdfName;
}
} elseif ($status === 'finalized' && !empty($draft->teacher_file)) {
$copied = $this->fileService->copyDraftToFinal(
(string) $draft->teacher_file,
ExamDraftConfigService::TEACHER_UPLOAD_DIR,
ExamDraftConfigService::FINAL_UPLOAD_DIR
);
if ($copied !== null) {
$update['final_file'] = $copied;
$update['final_filename'] = $draft->teacher_filename ?? $draft->teacher_file;
$pdfName = $this->fileService->ensurePdfExists(
$copied,
pathinfo($copied, PATHINFO_EXTENSION),
ExamDraftConfigService::FINAL_UPLOAD_DIR
);
if ($pdfName !== null && ($this->configService->context()['has_final_pdf'] ?? false)) {
$update['final_pdf_file'] = $pdfName;
}
}
}
if ($status === 'finalized' && ($this->configService->context()['has_legacy'] ?? false)) {
$update['is_legacy'] = 1;
}
$draft->update($update);
return ['ok' => true, 'draft' => $draft->fresh()->toArray()];
}
private function normalizeStatus(string $input, string $default): string
{
$input = strtolower(trim($input));
$aliases = [
'pending' => 'submitted',
'final' => 'finalized',
'approved' => 'finalized',
];
if (isset($aliases[$input])) {
$input = $aliases[$input];
}
return in_array($input, ExamDraftConfigService::STATUS_OPTIONS, true) ? $input : $default;
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Services\Exams;
use App\Models\Configuration;
use Illuminate\Support\Facades\Schema;
class ExamDraftConfigService
{
public const TEACHER_UPLOAD_DIR = 'exams/drafts';
public const FINAL_UPLOAD_DIR = 'exams/finals';
public const MAX_UPLOAD_BYTES = 12 * 1024 * 1024;
public const ALLOWED_EXTENSIONS = ['doc', 'docx'];
public const ADMIN_ALLOWED_EXTENSIONS = ['doc', 'docx', 'pdf'];
public const EXAM_TYPES = [
'Final Exam',
'Midterm Exam',
'Quiz',
'Study Guide',
'Practice Exam',
'Other',
];
public const STATUS_OPTIONS = ['draft', 'submitted', 'reviewed', 'finalized', 'rejected'];
public function context(): array
{
return [
'school_year' => (string) (Configuration::getConfig('school_year') ?? ''),
'semester' => (string) (Configuration::getConfig('semester') ?? ''),
'has_final_pdf' => $this->hasColumn('exam_drafts', 'final_pdf_file'),
'has_legacy' => $this->hasColumn('exam_drafts', 'is_legacy'),
];
}
private function hasColumn(string $table, string $column): bool
{
try {
return Schema::hasColumn($table, $column);
} catch (\Throwable) {
return false;
}
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace App\Services\Exams;
use Illuminate\Http\UploadedFile;
class ExamDraftFileService
{
public function storeUploadedFile(UploadedFile $file, string $subdir, array $allowedExtensions, int $maxBytes): ?array
{
$ext = strtolower($file->getClientOriginalExtension());
if (!in_array($ext, $allowedExtensions, true)) {
return null;
}
if ($file->getSize() > $maxBytes) {
return null;
}
$destination = storage_path('uploads/' . trim($subdir, '/'));
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
$filename = uniqid('exam_', true) . '.' . $ext;
try {
$file->move($destination, $filename);
return [
'stored' => $filename,
'original' => $file->getClientOriginalName(),
'extension' => $ext,
];
} catch (\Throwable) {
return null;
}
}
public function ensurePdfExists(string $finalFilename, ?string $originalExt, string $subdir): ?string
{
$pdfNeighbor = $this->neighborPdfIfExists($finalFilename, $subdir);
if ($pdfNeighbor !== null) {
return $pdfNeighbor;
}
$ext = strtolower((string) $originalExt);
if ($ext === 'pdf') {
$path = $this->fullUploadPath($subdir, $finalFilename);
return is_file($path) ? $finalFilename : null;
}
return $this->convertDocToPdf($this->fullUploadPath($subdir, $finalFilename), $subdir);
}
public function copyDraftToFinal(string $draftFilename, string $sourceDir, string $targetDir): ?string
{
$source = $this->fullUploadPath($sourceDir, $draftFilename);
if (!is_file($source)) {
return null;
}
$destinationDir = storage_path('uploads/' . trim($targetDir, '/'));
if (!is_dir($destinationDir)) {
mkdir($destinationDir, 0755, true);
}
$ext = pathinfo($draftFilename, PATHINFO_EXTENSION);
$destName = uniqid('final_', true) . '.' . $ext;
$destPath = $destinationDir . '/' . $destName;
if (!@copy($source, $destPath)) {
return null;
}
return $destName;
}
private function convertDocToPdf(string $sourcePath, string $targetSubdir): ?string
{
$ext = strtolower(pathinfo($sourcePath, PATHINFO_EXTENSION));
if (!in_array($ext, ['doc', 'docx'], true)) {
return null;
}
$targetDir = storage_path('uploads/' . trim($targetSubdir, '/'));
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
$base = pathinfo($sourcePath, PATHINFO_FILENAME);
$targetPath = $targetDir . '/' . $base . '.pdf';
$cmd = 'soffice --headless --convert-to pdf --outdir ' . escapeshellarg($targetDir) . ' ' . escapeshellarg($sourcePath) . ' 2>/dev/null';
@exec($cmd);
return is_file($targetPath) ? basename($targetPath) : null;
}
private function neighborPdfIfExists(string $filename, string $subdir): ?string
{
if ($filename === '') {
return null;
}
$path = $this->fullUploadPath($subdir, $filename);
$base = pathinfo($path, PATHINFO_FILENAME);
$dir = pathinfo($path, PATHINFO_DIRNAME);
$pdfPath = $dir . '/' . $base . '.pdf';
return is_file($pdfPath) ? basename($pdfPath) : null;
}
private function fullUploadPath(string $subdir, string $filename): string
{
return storage_path('uploads/' . trim($subdir, '/') . '/' . $filename);
}
}
@@ -0,0 +1,133 @@
<?php
namespace App\Services\Exams;
use App\Models\ExamDraft;
use App\Models\TeacherClass;
use Illuminate\Support\Facades\DB;
class ExamDraftTeacherService
{
public function __construct(
private ExamDraftConfigService $configService,
private ExamDraftFileService $fileService
) {
}
public function listForTeacher(int $teacherId): array
{
$context = $this->configService->context();
$schoolYear = (string) ($context['school_year'] ?? '');
$assignments = TeacherClass::getClassAssignmentsByUserId($teacherId, $schoolYear);
$classSectionIds = array_map(static fn ($a) => (int) $a['class_section_id'], $assignments);
$drafts = ExamDraft::query()
->where('teacher_id', $teacherId)
->orderByDesc('created_at')
->get()
->toArray();
$legacyExams = [];
if (($context['has_legacy'] ?? false) && !empty($classSectionIds)) {
$legacyExams = DB::table('exam_drafts as ed')
->select('ed.*', 'cs.class_section_name')
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ed.class_section_id')
->whereIn('ed.class_section_id', $classSectionIds)
->where('ed.is_legacy', 1)
->whereNotNull('ed.final_file')
->orderBy('cs.class_section_name')
->orderByDesc('ed.created_at')
->get()
->map(fn ($r) => (array) $r)
->all();
}
return [
'assignments' => $assignments,
'drafts' => $drafts,
'legacyExams' => $legacyExams,
'examTypes' => ExamDraftConfigService::EXAM_TYPES,
'statusOptions' => ExamDraftConfigService::STATUS_OPTIONS,
'schoolYear' => $schoolYear,
'semester' => (string) ($context['semester'] ?? ''),
];
}
public function store(int $teacherId, array $payload, ?\Illuminate\Http\UploadedFile $file): array
{
$context = $this->configService->context();
$schoolYear = (string) ($context['school_year'] ?? '');
$semester = (string) ($context['semester'] ?? '');
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
$examType = trim((string) ($payload['exam_type'] ?? ''));
$description = trim((string) ($payload['description'] ?? ''));
if ($classSectionId <= 0) {
return ['ok' => false, 'message' => 'Select a class section before submitting.'];
}
$assignment = TeacherClass::query()
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->first();
if (!$assignment) {
return ['ok' => false, 'message' => 'You are not assigned to the selected class section.'];
}
$teacherFile = null;
$teacherFilename = null;
if ($file) {
$stored = $this->fileService->storeUploadedFile(
$file,
ExamDraftConfigService::TEACHER_UPLOAD_DIR,
ExamDraftConfigService::ALLOWED_EXTENSIONS,
ExamDraftConfigService::MAX_UPLOAD_BYTES
);
if ($stored === null) {
return ['ok' => false, 'message' => 'Failed to store the uploaded file.'];
}
$teacherFile = $stored['stored'];
$teacherFilename = $stored['original'];
}
$existingDraft = ExamDraft::query()
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->orderByDesc('version')
->first();
$nextVersion = 1;
$previousId = null;
if ($existingDraft) {
$nextVersion = ((int) ($existingDraft->version ?? 1)) + 1;
$previousId = (int) ($existingDraft->id ?? 0) ?: null;
}
$title = $examType !== '' ? $examType : 'Exam Draft';
$draft = ExamDraft::query()->create([
'teacher_id' => $teacherId,
'class_section_id' => $classSectionId,
'semester' => $semester,
'school_year' => $schoolYear,
'exam_type' => $examType !== '' ? $examType : null,
'draft_title' => $title,
'description' => $description !== '' ? $description : null,
'teacher_file' => $teacherFile,
'teacher_filename' => $teacherFilename,
'status' => 'submitted',
'version' => $nextVersion,
'previous_draft_id' => $previousId,
]);
return [
'ok' => true,
'draft' => $draft->toArray(),
];
}
}
@@ -0,0 +1,127 @@
<?php
namespace App\Services\Parents;
use Illuminate\Support\Facades\DB;
class ParentAttendanceReportCalendarService
{
public function __construct(private ParentConfigService $configService)
{
}
/**
* @return array{0: array<int, array{value:string,label:string}>, 1: string}
*/
public function computeSundays(int $weeksBefore = 4, int $weeksAfter = 26): array
{
$today = new \DateTime('today');
$dow = (int) $today->format('w');
$thisSunday = clone $today;
if ($dow !== 0) {
$thisSunday->modify('last sunday');
}
$default = clone $today;
if ($dow !== 0) {
$default->modify('next sunday');
}
$schoolYear = (string) ($this->configService->context()['school_year'] ?? '');
$cap = $this->firstSundayOfJune($schoolYear);
$dates = [];
for ($i = $weeksBefore; $i >= 1; $i--) {
$dates[] = (clone $thisSunday)->modify('-' . $i . ' week');
}
$cursor = clone $thisSunday;
while ($cursor <= $cap) {
$dates[] = clone $cursor;
$cursor->modify('+1 week');
}
$rangeStart = reset($dates);
$rangeEnd = end($dates);
$rangeStartStr = $rangeStart instanceof \DateTime ? $rangeStart->format('Y-m-d') : null;
$rangeEndStr = $rangeEnd instanceof \DateTime ? $rangeEnd->format('Y-m-d') : null;
$noSchool = [];
if ($rangeStartStr && $rangeEndStr) {
$rows = DB::table('calendar_events')
->select('date')
->where('no_school', 1)
->groupStart()
->where('school_year', $schoolYear)
->orWhere('school_year IS NULL', null, false)
->groupEnd()
->where('date >=', $rangeStartStr)
->where('date <=', $rangeEndStr)
->get()
->toArray();
foreach ($rows as $row) {
$d = substr((string) ($row->date ?? ''), 0, 10);
if ($d !== '') {
$noSchool[$d] = true;
}
}
}
$filtered = [];
foreach ($dates as $d) {
$ymd = $d->format('Y-m-d');
if (!isset($noSchool[$ymd]) && $d >= $today && $d <= $cap) {
$filtered[] = $d;
}
}
$defaultYmd = $default->format('Y-m-d');
if (!empty($filtered)) {
$defaultYmd = $filtered[0]->format('Y-m-d');
}
$out = [];
foreach ($filtered as $d) {
$out[] = [
'value' => $d->format('Y-m-d'),
'label' => $d->format('m-d-Y'),
];
}
return [$out, $defaultYmd];
}
public function normalizeCutoffTime(string $raw, string $default = '09:00'): string
{
$raw = trim($raw);
if ($raw === '') {
return $default;
}
if (preg_match('/^(\\d{1,2}):(\\d{2})$/', $raw, $m)) {
$hh = str_pad((string) ((int) $m[1]), 2, '0', STR_PAD_LEFT);
return $hh . ':' . $m[2];
}
return $default;
}
public function firstSundayOfJune(string $schoolYear): \DateTime
{
$today = new \DateTime('today');
$endYear = null;
if (preg_match('/^(\\d{4})\\D(\\d{4})$/', $schoolYear, $m)) {
$endYear = (int) $m[2];
}
if ($endYear === null) {
$y = (int) $today->format('Y');
$endYear = ((int) $today->format('n') > 6) ? ($y + 1) : $y;
}
$juneFirst = new \DateTime(sprintf('%04d-06-01', $endYear));
if ((int) $juneFirst->format('w') !== 0) {
$juneFirst->modify('next sunday');
}
return $juneFirst;
}
}
@@ -0,0 +1,509 @@
<?php
namespace App\Services\Parents;
use App\Models\AttendanceData;
use App\Models\AttendanceRecord;
use App\Models\ParentAttendanceReport;
use App\Models\Student;
use App\Models\ClassSection;
use App\Services\SemesterRangeService;
use Illuminate\Support\Facades\DB;
class ParentAttendanceReportService
{
public function __construct(
private ParentConfigService $configService,
private ParentAttendanceReportCalendarService $calendarService,
private SemesterRangeService $semesterRangeService
) {
}
public function formData(int $parentId): array
{
$context = $this->configService->context();
$students = Student::query()
->where('parent_id', $parentId)
->orderBy('firstname')
->orderBy('lastname')
->get()
->toArray();
[$sundays, $defaultDate] = $this->calendarService->computeSundays();
$today = now()->toDateString();
$schoolYear = (string) ($context['school_year'] ?? '');
$previewRows = DB::table('parent_attendance_reports as par')
->select('par.*', 's.firstname', 's.lastname')
->leftJoin('students as s', 's.id', '=', 'par.student_id')
->where('par.parent_id', $parentId)
->where('par.school_year', $schoolYear)
->where('par.report_date', '>=', $today)
->orderBy('par.report_date')
->orderBy('s.firstname')
->orderBy('s.lastname')
->get()
->map(fn ($row) => (array) $row)
->all();
$cutoffThreshold = $this->calendarService->normalizeCutoffTime(
(string) ($context['parent_report_cutoff'] ?? ''),
'09:00'
);
return [
'students' => $students,
'today' => $today,
'sundays' => $sundays,
'defaultDate' => $defaultDate,
'myReports' => $previewRows,
'cutoffThreshold' => $cutoffThreshold,
];
}
public function submit(int $parentId, array $payload): array
{
$context = $this->configService->context();
$schoolYear = (string) ($context['school_year'] ?? '');
$semester = (string) ($context['semester'] ?? '');
$studentIds = array_values(array_filter(array_map('intval', (array) ($payload['student_ids'] ?? []))));
if (empty($studentIds)) {
throw new \RuntimeException('At least select one student.');
}
$datesInput = $payload['dates'] ?? ($payload['date'] ?? null);
$rawDates = [];
if (is_array($datesInput)) {
$rawDates = $datesInput;
} elseif ($datesInput !== null) {
$rawDates = [$datesInput];
}
$rawDates = array_values(array_unique(array_filter(array_map(static function ($val) {
return substr((string) $val, 0, 10);
}, $rawDates))));
if (empty($rawDates)) {
throw new \RuntimeException('Please select at least one Sunday date.');
}
$type = (string) ($payload['type'] ?? '');
if (!in_array($type, ['absent', 'late', 'early_dismissal'], true)) {
throw new \RuntimeException('Invalid report type.');
}
$arrivalTime = $payload['arrival_time'] ?? null;
$dismissTime = $payload['dismiss_time'] ?? null;
$reason = isset($payload['reason']) ? trim((string) $payload['reason']) : null;
$todayCheck = new \DateTime('today');
$cap = $this->calendarService->firstSundayOfJune($schoolYear);
$validDates = [];
foreach ($rawDates as $entry) {
$dt = \DateTime::createFromFormat('Y-m-d', $entry);
if (!$dt) {
throw new \RuntimeException('Invalid date selected: ' . $entry);
}
if ((int) $dt->format('w') !== 0) {
throw new \RuntimeException('Please select Sunday dates only.');
}
if ($dt < $todayCheck) {
throw new \RuntimeException('Past dates are not allowed. Please select today or a future Sunday.');
}
if ($dt > $cap) {
throw new \RuntimeException('The last available date is the first Sunday of June.');
}
$validDates[$entry] = $dt;
}
if ($type === 'late' && (!is_string($arrivalTime) || $arrivalTime === '')) {
throw new \RuntimeException('Please provide an expected arrival time for late reporting.');
}
if ($type === 'early_dismissal' && (!is_string($dismissTime) || $dismissTime === '')) {
throw new \RuntimeException('Please provide a dismissal time for early dismissal.');
}
if ($type !== 'early_dismissal') {
if ($reason === null || $reason === '') {
throw new \RuntimeException('Please provide a reason for absence or late arrival.');
}
}
$inserted = 0;
$blocked = [];
$successDetails = [];
foreach ($studentIds as $sid) {
if ($sid <= 0) {
continue;
}
$sectionRow = DB::table('student_class')
->select('class_section_id')
->where('student_id', $sid)
->where('school_year', $schoolYear)
->orderByDesc('updated_at')
->first();
$classSectionId = $sectionRow->class_section_id ?? null;
$student = Student::query()->select('firstname', 'lastname')->find($sid);
$studentName = $student ? trim($student->firstname . ' ' . $student->lastname) : ('Student #' . $sid);
$classLabel = $this->resolveClassSectionLabel($classSectionId);
foreach ($validDates as $reportDate => $_dt) {
$semesterForDate = $this->semesterRangeService->getSemesterForDate($reportDate) ?: $semester;
$saved = false;
$qb = ParentAttendanceReport::query()
->where('student_id', $sid)
->where('report_date', $reportDate);
if ($type === 'early_dismissal') {
$existingAL = (clone $qb)
->whereIn('type', ['absent', 'late'])
->first();
if ($existingAL) {
$blocked[] = $studentName . ' (' . $reportDate . ')';
} else {
$existingED = (clone $qb)->where('type', 'early_dismissal')->first();
if ($existingED) {
$existingED->update([
'dismiss_time' => $dismissTime ?: $existingED->dismiss_time,
'status' => 'new',
]);
$saved = true;
$inserted++;
} else {
ParentAttendanceReport::query()->create([
'parent_id' => $parentId,
'student_id' => $sid,
'class_section_id' => $classSectionId,
'report_date' => $reportDate,
'type' => 'early_dismissal',
'dismiss_time' => $dismissTime ?: null,
'reason' => $reason ?: null,
'semester' => $semesterForDate,
'school_year' => $schoolYear,
'status' => 'new',
]);
$saved = true;
$inserted++;
}
}
} else {
$existingAny = (clone $qb)
->whereIn('type', ['absent', 'late', 'early_dismissal'])
->first();
if ($existingAny) {
$blocked[] = $studentName . ' (' . $reportDate . ')';
} else {
ParentAttendanceReport::query()->create([
'parent_id' => $parentId,
'student_id' => $sid,
'class_section_id' => $classSectionId,
'report_date' => $reportDate,
'type' => $type,
'arrival_time' => ($type === 'late') ? ($arrivalTime ?: null) : null,
'reason' => $reason ?: null,
'semester' => $semesterForDate,
'school_year' => $schoolYear,
'status' => 'new',
]);
$saved = true;
$inserted++;
}
}
if ($saved && in_array($type, ['absent', 'late'], true)) {
$this->reflectToAttendanceData(
studentId: $sid,
reportDate: $reportDate,
type: $type,
semester: $semesterForDate,
schoolYear: $schoolYear,
classSectionId: $classSectionId,
reason: $reason,
arrivalTime: $arrivalTime
);
}
if ($saved) {
$successDetails[] = [
'student_id' => $sid,
'name' => $studentName,
'class_label' => $classLabel,
'type' => $type,
'arrival_time' => $arrivalTime,
'dismiss_time' => $dismissTime,
'date' => $reportDate,
];
}
}
}
if ($inserted <= 0) {
$err = !empty($blocked)
? ('Already submitted and cannot be changed for: ' . implode(', ', array_slice($blocked, 0, 5)) . (count($blocked) > 5 ? '…' : ''))
: 'Could not save your report. Please try again.';
throw new \RuntimeException($err);
}
return [
'inserted' => $inserted,
'blocked' => $blocked,
'students' => $successDetails,
'dates' => array_keys($validDates),
];
}
public function listReports(string $start, ?string $end, ?string $schoolYear, ?string $semester): array
{
return ParentAttendanceReport::listForDateRange($start, $end, $schoolYear, $semester);
}
public function checkExisting(array $payload): array
{
$datesPost = $payload['dates'] ?? null;
$dateSingle = $payload['date'] ?? null;
$dateValues = [];
if (is_array($datesPost)) {
foreach ($datesPost as $d) {
$dateValues[] = substr((string) $d, 0, 10);
}
} elseif ($dateSingle !== null) {
$dateValues[] = substr((string) $dateSingle, 0, 10);
}
$dateValues = array_values(array_unique(array_filter($dateValues, static function ($d) {
return preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $d);
})));
$type = (string) ($payload['type'] ?? '');
if (empty($dateValues) || !in_array($type, ['absent', 'late', 'early_dismissal'], true)) {
throw new \RuntimeException('Invalid date or type.');
}
$studentIds = array_values(array_unique(array_map('intval', (array) ($payload['student_ids'] ?? []))));
$studentIds = array_filter($studentIds, static fn ($v) => $v > 0);
if (empty($studentIds)) {
return [];
}
$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')
->whereIn('par.report_date', $dateValues)
->whereIn('par.student_id', $studentIds)
->get()
->map(fn ($row) => (array) $row)
->all();
$map = [];
foreach ($rows as $row) {
$sid = (int) ($row['student_id'] ?? 0);
if ($sid <= 0) {
continue;
}
if (!isset($map[$sid])) {
$map[$sid] = [
'student_id' => $sid,
'firstname' => (string) ($row['firstname'] ?? ''),
'lastname' => (string) ($row['lastname'] ?? ''),
'dates' => [],
];
}
$dVal = substr((string) ($row['report_date'] ?? ''), 0, 10);
if ($dVal === '') {
continue;
}
$map[$sid]['dates'][$dVal] = $map[$sid]['dates'][$dVal] ?? [];
$t = (string) ($row['type'] ?? '');
if ($t !== '' && !in_array($t, $map[$sid]['dates'][$dVal], true)) {
$map[$sid]['dates'][$dVal][] = $t;
}
}
return array_values($map);
}
public function updateReport(int $parentId, int $reportId, array $payload): void
{
$row = ParentAttendanceReport::query()->find($reportId);
if (!$row || (int) $row->parent_id !== $parentId) {
throw new \RuntimeException('Report not found.');
}
if ((string) ($row->status ?? '') !== 'new') {
throw new \RuntimeException('This report can no longer be edited.');
}
$tzName = (string) (config('School')->attendance['timezone'] ?? 'UTC');
$tz = new \DateTimeZone($tzName ?: 'UTC');
$cutoff = new \DateTime($row->report_date . ' 09:00:00', $tz);
$now = new \DateTime('now', $tz);
if ($now >= $cutoff) {
throw new \RuntimeException('Editing window closed at 9:00 AM on the report date.');
}
$type = (string) $row->type;
$reason = trim((string) ($payload['reason'] ?? ''));
$arrival = (string) ($payload['arrival_time'] ?? '');
$dismiss = (string) ($payload['dismiss_time'] ?? '');
$update = [
'reason' => $reason !== '' ? $reason : null,
];
if ($type === 'late' && $arrival !== '') {
if (!preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $arrival)) {
throw new \RuntimeException('Invalid arrival time format.');
}
$update['arrival_time'] = $arrival;
}
if ($type === 'early_dismissal' && $dismiss !== '') {
if (!preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $dismiss)) {
throw new \RuntimeException('Invalid dismissal time format.');
}
$update['dismiss_time'] = $dismiss;
}
if (empty($update)) {
throw new \RuntimeException('Nothing to update.');
}
$row->update($update);
}
private function resolveClassSectionLabel(?int $classSectionId): string
{
$cid = (int) ($classSectionId ?? 0);
if ($cid <= 0) {
return 'Not Assigned';
}
$name = ClassSection::query()
->where('class_section_id', $cid)
->value('class_section_name');
return $name ?: 'Not Assigned';
}
private function reflectToAttendanceData(
int $studentId,
string $reportDate,
string $type,
string $semester,
string $schoolYear,
?int $classSectionId,
?string $reason = null,
?string $arrivalTime = null
): void {
$status = ($type === 'late') ? 'late' : (($type === 'absent') ? 'absent' : null);
if ($status === null) {
return;
}
if (!$classSectionId) {
$row = DB::table('student_class')
->select('class_section_id')
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->orderByDesc('updated_at')
->first();
$classSectionId = (int) ($row->class_section_id ?? 0);
}
if ($classSectionId <= 0) {
return;
}
$existing = AttendanceData::query()
->where('student_id', $studentId)
->where('class_section_id', $classSectionId)
->where('date', $reportDate)
->where('semester', $semester)
->where('school_year', $schoolYear)
->first();
$reasonParts = ['Parent-reported'];
if ($type === 'late' && $arrivalTime) {
$reasonParts[] = 'ETA ' . substr($arrivalTime, 0, 5);
}
if (!empty($reason)) {
$reasonParts[] = trim($reason);
}
$reasonText = implode(' — ', $reasonParts);
if (!$existing) {
$classId = ClassSection::query()
->where('class_section_id', $classSectionId)
->value('class_id');
$schoolId = Student::query()->whereKey($studentId)->value('school_id');
if (!$classId || !$schoolId) {
return;
}
AttendanceData::query()->create([
'class_id' => $classId,
'class_section_id' => $classSectionId,
'student_id' => $studentId,
'school_id' => $schoolId,
'date' => $reportDate,
'status' => $status,
'reason' => $reasonText,
'semester' => $semester,
'school_year' => $schoolYear,
]);
$this->bumpAttendanceRecord($studentId, $classSectionId, $schoolId, $status, $semester, $schoolYear);
return;
}
$existingStatus = strtolower((string) ($existing->status ?? ''));
if ($existingStatus === 'present' || $existingStatus === '') {
$existing->update([
'status' => $status,
'reason' => $existing->reason ?: $reasonText,
]);
}
}
private function bumpAttendanceRecord(
int $studentId,
int $classSectionId,
string $schoolId,
string $status,
string $semester,
string $schoolYear
): void {
$record = AttendanceRecord::query()
->where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->first();
if ($record) {
$updates = [
'total_presence' => (int) ($record->total_presence ?? 0),
'total_absence' => (int) ($record->total_absence ?? 0),
'total_late' => (int) ($record->total_late ?? 0),
'total_attendance' => (int) ($record->total_attendance ?? 0) + 1,
];
if ($status === 'present') {
$updates['total_presence']++;
} elseif ($status === 'absent') {
$updates['total_absence']++;
} elseif ($status === 'late') {
$updates['total_late']++;
}
$record->update($updates);
return;
}
AttendanceRecord::query()->create([
'class_section_id' => $classSectionId,
'student_id' => $studentId,
'school_id' => $schoolId,
'semester' => $semester,
'school_year' => $schoolYear,
'total_presence' => ($status === 'present') ? 1 : 0,
'total_absence' => ($status === 'absent') ? 1 : 0,
'total_late' => ($status === 'late') ? 1 : 0,
'total_attendance' => 1,
]);
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Services\Parents;
use Illuminate\Support\Facades\DB;
class ParentAttendanceService
{
public function __construct(private ParentConfigService $configService)
{
}
public function listAttendance(int $parentId, ?string $schoolYear = null): array
{
$context = $this->configService->context();
$selectedYear = $schoolYear ?: $context['school_year'];
$rows = DB::table('attendance_data')
->select('students.firstname', 'students.lastname', 'attendance_data.date', 'attendance_data.status', 'attendance_data.reason')
->join('students', 'students.id', '=', 'attendance_data.student_id')
->where('students.parent_id', $parentId)
->where('attendance_data.school_year', $selectedYear)
->orderBy('attendance_data.date', 'desc')
->get()
->map(fn ($row) => (array) $row)
->all();
$schoolYears = DB::table('attendance_data')
->select('school_year')
->distinct()
->orderBy('school_year', 'desc')
->get()
->map(fn ($row) => (array) $row)
->all();
return [
'attendance' => $rows,
'schoolYears' => $schoolYears,
'selectedYear' => $selectedYear,
];
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Services\Parents;
use App\Models\Configuration;
class ParentConfigService
{
public function context(): array
{
return [
'date_age_reference' => Configuration::getConfig('date_age_reference'),
'enrollment_deadline' => Configuration::getConfig('enrollment_deadline'),
'fall_semester_start' => Configuration::getConfig('fall_semester_start'),
'refund_deadline' => Configuration::getConfig('refund_deadline'),
'school_year' => Configuration::getConfig('school_year'),
'semester' => Configuration::getConfig('semester'),
'max_kids' => (int) (Configuration::getConfig('max_kids') ?? 0),
'max_emergency' => (int) (Configuration::getConfig('max_emergency') ?? 0),
];
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Services\Parents;
use App\Models\EmergencyContact;
use App\Services\PhoneFormatterService;
class ParentEmergencyContactService
{
public function __construct(
private ParentConfigService $configService,
private PhoneFormatterService $phoneFormatter
) {
}
public function list(int $parentId): array
{
return EmergencyContact::query()
->where('parent_id', $parentId)
->orderBy('emergency_contact_name')
->get()
->toArray();
}
public function store(int $parentId, array $payload): array
{
$context = $this->configService->context();
$contact = EmergencyContact::query()->create([
'parent_id' => $parentId,
'emergency_contact_name' => $payload['name'],
'cellphone' => $this->phoneFormatter->formatPhoneNumber($payload['cellphone']),
'email' => $payload['email'] ?? null,
'relation' => $payload['relation'] ?? null,
'semester' => $context['semester'],
'school_year' => $context['school_year'],
]);
return $contact->toArray();
}
public function update(int $parentId, int $contactId, array $payload): array
{
$contact = EmergencyContact::query()
->where('id', $contactId)
->where('parent_id', $parentId)
->firstOrFail();
$contact->update([
'emergency_contact_name' => $payload['name'],
'cellphone' => $this->phoneFormatter->formatPhoneNumber($payload['cellphone']),
'email' => $payload['email'] ?? null,
'relation' => $payload['relation'] ?? null,
]);
return $contact->toArray();
}
}
@@ -0,0 +1,206 @@
<?php
namespace App\Services\Parents;
use App\Models\Enrollment;
use App\Models\Refund;
use App\Models\Student;
use App\Models\StudentClass;
use Illuminate\Support\Facades\DB;
class ParentEnrollmentService
{
public function __construct(private ParentConfigService $configService)
{
}
public function overview(int $parentId, ?string $schoolYear = null): array
{
$context = $this->configService->context();
$selectedYear = $schoolYear ?: $context['school_year'];
$students = Student::query()
->where('parent_id', $parentId)
->orderBy('lastname')
->orderBy('firstname')
->get()
->map(function ($student) use ($selectedYear) {
$classSections = StudentClass::getClassSectionsByStudentId($student->id, $selectedYear, true);
$student->class_section = !empty($classSections)
? implode(', ', $classSections)
: 'Class not Assigned';
$isArabicClass = !empty($classSections) && array_reduce(
$classSections,
static fn ($carry, $name) => $carry || (is_string($name) && stripos($name, 'arabic') === 0),
false
);
$enrollment = Enrollment::query()
->select('enrollment_status', 'admission_status')
->where('student_id', $student->id)
->where('school_year', $selectedYear)
->first();
$statusMap = [
'admission under review' => 'admission under review',
'payment pending' => 'payment pending',
'enrolled' => 'enrolled',
'withdraw under review' => 'withdraw under review',
'refund pending' => 'refund pending',
'withdrawn' => 'withdrawn',
'denied' => 'denied',
'waitlist' => 'waitlist',
];
if ($enrollment && isset($enrollment->admission_status)) {
$student->admission_status = $enrollment->admission_status;
if ($enrollment->admission_status === 'denied') {
$student->enrollment_status = 'denied';
} else {
$student->enrollment_status = $statusMap[$enrollment->enrollment_status] ?? 'not enrolled';
}
} else {
$student->admission_status = null;
$student->enrollment_status = 'not enrolled';
}
if ($student->enrollment_status === 'not enrolled' && $isArabicClass) {
$student->enrollment_status = 'enrolled';
}
$student->disable_enroll = in_array(
$student->enrollment_status,
['admission under review', 'payment pending', 'enrolled', 'withdraw under review', 'denied'],
true
);
return $student->toArray();
})
->all();
$schoolYears = DB::table('enrollments')
->select('school_year')
->distinct()
->orderBy('school_year', 'desc')
->get()
->map(fn ($row) => (array) $row)
->all();
if (empty($schoolYears)) {
$schoolYears[] = ['school_year' => $selectedYear];
}
return [
'students' => $students,
'schoolYears' => $schoolYears,
'selectedYear' => $selectedYear,
'withdrawalDeadline' => $context['refund_deadline'],
'lastDayOfRegistration' => $context['enrollment_deadline'],
'schoolStartDate' => $context['fall_semester_start'],
];
}
public function updateEnrollment(int $parentId, array $enrollIds, array $withdrawIds): array
{
$context = $this->configService->context();
$schoolYear = $context['school_year'];
$semester = $context['semester'];
$enrolled = [];
$withdrawn = [];
foreach ($enrollIds as $studentId) {
$existing = Enrollment::query()
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->first();
if ($existing) {
if ((int) $existing->is_withdrawn === 1) {
$existing->update([
'is_withdrawn' => 0,
'withdrawal_date' => null,
'enrollment_status' => 'payment pending',
'updated_at' => now(),
]);
}
} else {
Enrollment::query()->create([
'student_id' => $studentId,
'parent_id' => $parentId,
'school_year' => $schoolYear,
'semester' => $semester,
'enrollment_date' => now()->toDateString(),
'is_withdrawn' => 0,
'enrollment_status' => 'admission under review',
'admission_status' => 'pending',
'created_at' => now(),
]);
}
$enrolled[] = (int) $studentId;
}
foreach ($withdrawIds as $studentId) {
$enrollment = Enrollment::query()
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->where('semester', $semester)
->where('is_withdrawn', 0)
->first();
if (!$enrollment) {
continue;
}
$enrollment->update([
'withdrawal_date' => now()->toDateString(),
'enrollment_status' => 'withdraw under review',
'updated_at' => now(),
]);
$invoice = DB::table('invoices')
->where('parent_id', $parentId)
->where('school_year', $schoolYear)
->orderByDesc('created_at')
->first();
if ($invoice) {
$refund = Refund::query()
->where('parent_id', $parentId)
->where('invoice_id', $invoice->id)
->where('school_year', $schoolYear)
->first();
if ($refund) {
$refund->update([
'reason' => 'Withdrawal under review for student ID ' . $studentId,
'note' => null,
'updated_by' => $parentId,
]);
} else {
Refund::query()->create([
'parent_id' => $parentId,
'invoice_id' => $invoice->id,
'requested_at' => now(),
'school_year' => $schoolYear,
'status' => Refund::STATUS_PENDING,
'reason' => 'Withdrawal under review for student ID ' . $studentId,
'request' => 'new',
'semester' => $semester,
'refund_paid_amount' => 0.0,
]);
}
}
$withdrawn[] = (int) $studentId;
}
return [
'enrolled' => $enrolled,
'withdrawn' => $withdrawn,
];
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Services\Parents;
use App\Models\Event;
use App\Models\EventCharges;
use App\Models\Enrollment;
use App\Services\Invoices\InvoiceGenerationService;
class ParentEventParticipationService
{
public function __construct(
private ParentConfigService $configService,
private InvoiceGenerationService $invoiceGeneration
) {
}
public function overview(int $parentId): array
{
$context = $this->configService->context();
$schoolYear = $context['school_year'];
$semester = $context['semester'];
$activeEvents = Event::getActiveEvents($schoolYear, $semester) ?? [];
$chargesList = EventCharges::getChargesWithEventInfo($parentId, $schoolYear) ?? [];
$charges = [];
foreach ($chargesList as $charge) {
$key = $charge['student_id'] . ':' . $charge['event_id'];
$charges[$key] = [
'participation' => $charge['participation'],
'date' => $charge['updated_at'] ?? $charge['created_at'],
];
}
$students = Enrollment::getEnrolledStudents($parentId, $schoolYear);
return [
'activeEvents' => $activeEvents,
'charges' => $charges,
'yourStudents' => $students,
];
}
public function updateParticipation(int $parentId, array $participations): void
{
$context = $this->configService->context();
$schoolYear = $context['school_year'];
$semester = $context['semester'];
foreach ($participations as $key => $value) {
[$studentId, $eventId] = array_map('intval', explode(':', (string) $key));
$existing = EventCharges::query()
->where('parent_id', $parentId)
->where('student_id', $studentId)
->where('event_id', $eventId)
->first();
if ($value === 'no') {
if ($existing) {
$existing->delete();
}
continue;
}
if ($existing) {
$existing->update(['participation' => $value]);
} else {
$event = Event::getEvent($eventId, $schoolYear);
EventCharges::query()->create([
'parent_id' => $parentId,
'student_id' => $studentId,
'event_id' => $eventId,
'participation' => $value,
'charged' => $event['amount'] ?? 0,
'school_year' => $schoolYear,
'semester' => $semester,
'updated_by' => $parentId,
]);
}
}
$this->invoiceGeneration->generateInvoice($parentId, $schoolYear);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Services\Parents;
use App\Models\Invoice;
class ParentInvoiceService
{
public function listInvoices(int $parentId, ?string $schoolYear = null): array
{
$rows = Invoice::query()
->where('parent_id', $parentId)
->when($schoolYear, fn ($q) => $q->where('school_year', $schoolYear))
->orderByDesc('created_at')
->get();
return $rows->toArray();
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Services\Parents;
use App\Models\User;
class ParentProfileService
{
public function getProfile(int $parentId): ?array
{
$user = User::query()->find($parentId);
return $user ? $user->toArray() : null;
}
public function updateProfile(int $parentId, array $payload): array
{
$user = User::query()->findOrFail($parentId);
$user->update([
'firstname' => $payload['firstname'],
'lastname' => $payload['lastname'],
'cellphone' => $payload['cellphone'],
'address_street' => $payload['address_street'],
'city' => $payload['city'],
'state' => $payload['state'],
'zip' => $payload['zip'],
]);
return $user->toArray();
}
}
@@ -0,0 +1,228 @@
<?php
namespace App\Services\Parents;
use App\Models\EmergencyContact;
use App\Models\Student;
use App\Models\StudentAllergy;
use App\Models\StudentMedicalCondition;
use App\Models\User;
use App\Services\SchoolIdService;
use Illuminate\Support\Facades\DB;
class ParentRegistrationService
{
public function __construct(
private ParentConfigService $configService,
private SchoolIdService $schoolIdService
) {
}
public function overview(int $parentId): array
{
$context = $this->configService->context();
$parent = User::query()->find($parentId);
$kids = Student::query()->where('parent_id', $parentId)->get();
$kidIds = $kids->pluck('id')->all();
$allergies = StudentAllergy::query()
->whereIn('student_id', $kidIds)
->get()
->groupBy('student_id');
$conditions = StudentMedicalCondition::query()
->whereIn('student_id', $kidIds)
->get()
->groupBy('student_id');
$kids = $kids->map(function ($kid) use ($allergies, $conditions) {
$kid->allergies = ($allergies[$kid->id] ?? collect())->pluck('allergy')->all();
$kid->medical_conditions = ($conditions[$kid->id] ?? collect())->pluck('condition_name')->all();
return $kid->toArray();
})->all();
$emergencies = EmergencyContact::query()->where('parent_id', $parentId)->get()->toArray();
return [
'parent' => $parent ? $parent->toArray() : null,
'existingKids' => $kids,
'emergencies' => $emergencies,
'maxChilds' => $context['max_kids'],
'maxEmergency' => $context['max_emergency'],
'lastDayOfRegistration' => $context['enrollment_deadline'],
'registrationAgeDeadline' => $context['date_age_reference'],
];
}
public function register(int $parentId, array $students, array $emergencyContacts): array
{
$context = $this->configService->context();
$schoolYear = $context['school_year'];
$semester = $context['semester'];
$ageReference = $context['date_age_reference'] ?: now()->toDateString();
$existingKidsCount = Student::query()->where('parent_id', $parentId)->count();
$existingECCount = EmergencyContact::query()->where('parent_id', $parentId)->count();
if ($existingKidsCount + count($students) > $context['max_kids']) {
throw new \RuntimeException('Student limit exceeded.');
}
if ($existingECCount + count($emergencyContacts) > $context['max_emergency']) {
throw new \RuntimeException('Emergency contact limit exceeded.');
}
$createdStudentIds = [];
DB::transaction(function () use (
$parentId,
$students,
$emergencyContacts,
$schoolYear,
$semester,
&$createdStudentIds
) {
foreach ($students as $student) {
$exists = Student::query()
->where('school_year', $schoolYear)
->where('dob', $student['dob'])
->where('firstname', $student['firstname'])
->where('lastname', $student['lastname'])
->first();
if ($exists) {
continue;
}
$row = Student::query()->create([
'firstname' => $student['firstname'],
'lastname' => $student['lastname'],
'dob' => $student['dob'],
'age' => $this->calculateAge($student['dob'], $ageReference),
'gender' => $student['gender'],
'registration_grade' => $student['registration_grade'] ?? null,
'photo_consent' => !empty($student['photo_consent']) ? 1 : 0,
'parent_id' => $parentId,
'year_of_registration' => (string) date('Y'),
'school_year' => $schoolYear,
'semester' => $semester,
'is_new' => isset($student['is_new']) ? (int) (bool) $student['is_new'] : null,
'registration_date' => now(),
'tuition_paid' => 0,
'school_id' => $this->schoolIdService->generateStudentSchoolId(),
]);
$createdStudentIds[] = (int) $row->id;
foreach (($student['medical_conditions'] ?? []) as $condition) {
$condition = trim((string) $condition);
if ($condition === '') {
continue;
}
StudentMedicalCondition::query()->create([
'student_id' => $row->id,
'condition_name' => $condition,
]);
}
foreach (($student['allergies'] ?? []) as $allergy) {
$allergy = trim((string) $allergy);
if ($allergy === '') {
continue;
}
StudentAllergy::query()->create([
'student_id' => $row->id,
'allergy' => $allergy,
]);
}
}
foreach ($emergencyContacts as $contact) {
EmergencyContact::query()->create([
'parent_id' => $parentId,
'emergency_contact_name' => $contact['name'],
'cellphone' => $contact['cellphone'],
'email' => $contact['email'] ?? null,
'relation' => $contact['relation'] ?? null,
'semester' => $semester,
'school_year' => $schoolYear,
]);
}
});
return [
'created_student_ids' => $createdStudentIds,
];
}
public function updateStudent(int $parentId, int $studentId, array $payload): void
{
$student = Student::query()
->where('id', $studentId)
->where('parent_id', $parentId)
->firstOrFail();
$ageReference = $this->configService->context()['date_age_reference'] ?: now()->toDateString();
$student->update([
'firstname' => $payload['firstname'],
'lastname' => $payload['lastname'],
'dob' => $payload['dob'],
'age' => $this->calculateAge($payload['dob'], $ageReference),
'gender' => $payload['gender'],
'registration_grade' => $payload['registration_grade'] ?? null,
'photo_consent' => !empty($payload['photo_consent']) ? 1 : 0,
]);
StudentMedicalCondition::query()->where('student_id', $studentId)->delete();
foreach (($payload['medical_conditions'] ?? []) as $condition) {
$condition = trim((string) $condition);
if ($condition === '') {
continue;
}
StudentMedicalCondition::query()->create([
'student_id' => $studentId,
'condition_name' => $condition,
]);
}
StudentAllergy::query()->where('student_id', $studentId)->delete();
foreach (($payload['allergies'] ?? []) as $allergy) {
$allergy = trim((string) $allergy);
if ($allergy === '') {
continue;
}
StudentAllergy::query()->create([
'student_id' => $studentId,
'allergy' => $allergy,
]);
}
}
public function deleteStudent(int $parentId, int $studentId): void
{
$student = Student::query()
->where('id', $studentId)
->where('parent_id', $parentId)
->firstOrFail();
$student->delete();
$remaining = Student::query()->where('parent_id', $parentId)->count();
if ($remaining === 0) {
EmergencyContact::query()->where('parent_id', $parentId)->delete();
}
}
private function calculateAge(string $dob, string $referenceDate): int
{
try {
$dobObj = new \DateTimeImmutable($dob);
$refObj = new \DateTimeImmutable($referenceDate);
return (int) $dobObj->diff($refObj)->y;
} catch (\Throwable $e) {
return 0;
}
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Services\Reports;
use App\Models\Configuration;
use App\Models\User;
class SlipPrinterConfigService
{
public function context(): array
{
return [
'school_year' => Configuration::getConfig('school_year'),
'semester' => Configuration::getConfig('semester'),
];
}
public function currentAdminName(?int $userId): string
{
if ($userId && $userId > 0) {
$user = User::query()->select('firstname', 'lastname')->find($userId);
if ($user) {
$full = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? ''));
if ($full !== '') {
return $full;
}
}
}
return '';
}
public function printerConfig(): array
{
$get = function ($keys, $default = null) {
$keys = is_array($keys) ? $keys : [$keys];
foreach ($keys as $key) {
$val = env($key);
if ($val !== null && $val !== '') {
return $val;
}
}
return $default;
};
return [
'chars_per_line' => (int) $get(['printer.chars_per_line', 'PRINTER_CHARS_PER_LINE'], 48),
'feed_lines' => (int) $get(['printer.feed_lines', 'PRINTER_FEED_LINES'], 3),
'paper' => (string) $get(['printer.paper', 'SLIP_PAPER'], 'card'),
];
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Services\Reports;
class SlipPrinterFormatterService
{
public function toDbDate(?string $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
return $value;
}
if (preg_match('/^\d{1,2}\/\d{1,2}\/\d{4}$/', $value)) {
$dt = \DateTime::createFromFormat('!m/d/Y', $value);
return $dt ? $dt->format('Y-m-d') : null;
}
if (preg_match('/^\d{1,2}-\d{1,2}-\d{4}$/', $value)) {
$dt = \DateTime::createFromFormat('!m-d-Y', $value);
return $dt ? $dt->format('Y-m-d') : null;
}
$ts = strtotime($value);
return $ts ? date('Y-m-d', $ts) : null;
}
public function toDbTime(?string $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
$ts = strtotime($value);
return $ts ? date('H:i:s', $ts) : null;
}
public function formatDisplayDate(?string $value): string
{
$value = trim((string) $value);
if ($value === '' || $value === '0000-00-00') {
return '';
}
$ts = strtotime($value);
return $ts ? date('m/d/Y', $ts) : '';
}
public function formatDisplayTime(?string $value): string
{
$value = trim((string) $value);
if ($value === '') {
return '';
}
$ts = strtotime($value);
if ($ts === false) {
$ts = strtotime('today ' . $value);
}
return $ts ? date('h:i A', $ts) : '';
}
public function fitLine(string $line, int $width): string
{
$line = (string) $line;
if (strlen($line) > $width) {
return substr($line, 0, $width);
}
return $line . str_repeat(' ', $width - strlen($line));
}
public function buildSlipLines(array $data, int $width): array
{
$lines = [];
$lines[] = $this->fitLine('Student Name: ' . (string) ($data['student_name'] ?? ''), $width);
$lines[] = $this->fitLine('Date: ' . (string) ($data['date'] ?? '') . ' Time In: ' . (string) ($data['time_in'] ?? ''), $width);
$lines[] = $this->fitLine('Grade: ' . (string) ($data['grade'] ?? ''), $width);
$lines[] = $this->fitLine('Reason: ' . (string) ($data['reason'] ?? ''), $width);
$lines[] = $this->fitLine('Admin: ' . (string) ($data['admin_name'] ?? ''), $width);
return $lines;
}
}
@@ -0,0 +1,88 @@
<?php
namespace App\Services\Reports;
use Dompdf\Dompdf;
use Dompdf\Options;
class SlipPrinterPdfService
{
public function __construct(private SlipPrinterConfigService $configService)
{
}
public function render(array $data, array $lines, array $options = []): array
{
$paper = strtolower((string) ($options['paper'] ?? $this->configService->printerConfig()['paper'] ?? 'card'));
$fontPt = (float) ($options['font_pt'] ?? env('SLIP_FONT_PT', 13.0));
$lineH = (float) ($options['line_h'] ?? env('SLIP_LINE_H', 2.0));
$linesCount = (int) ($options['rows'] ?? count($lines));
if ($linesCount < 1) {
$linesCount = count($lines);
}
$ptPerLine = $fontPt * $lineH;
$mmPerPt = 25.4 / 72.0;
$contentMm = $ptPerLine * $linesCount * $mmPerPt;
$pageMarginsMm = 6.0;
$fudgeMm = (float) ($options['fudge_mm'] ?? env('SLIP_FUDGE_MM', 12.0));
$dynHeightMm = $contentMm + $pageMarginsMm + $fudgeMm;
switch ($paper) {
case 'cardp':
case 'card-portrait':
$wMm = 53.98;
$hMm = max($dynHeightMm, 40.0);
break;
case 'receipt80':
$wMm = 72.0;
$hMm = 90.0;
break;
case 'receipt58':
$wMm = 58.0;
$hMm = 80.0;
break;
case 'card':
default:
$wMm = 85.60;
$hMm = max($dynHeightMm, 40.0);
}
$hOverride = $options['height_mm'] ?? $options['h'] ?? null;
if (is_numeric($hOverride)) {
$hMm = max(30.0, min(200.0, (float) $hOverride));
}
if ($hMm < $dynHeightMm) {
$hMm = $dynHeightMm;
}
$html = view('slips/slip_pdf', [
'entry' => $data,
'lines' => $lines,
'page' => ['w_mm' => $wMm, 'h_mm' => $hMm],
]);
$optionsObj = new Options();
$optionsObj->set('isRemoteEnabled', true);
$optionsObj->set('defaultFont', 'Courier');
$dompdf = new Dompdf($optionsObj);
$dompdf->loadHtml($html, 'UTF-8');
$mmToPt = 72 / 25.4;
$wPt = $wMm * $mmToPt;
$hPt = $hMm * $mmToPt;
$dompdf->setPaper([0, 0, $wPt, $hPt]);
$dompdf->render();
$filename = 'LateSlip_' . preg_replace('/[^A-Za-z0-9]+/', '_', (string) ($data['student_name'] ?? 'slip')) . '_' . date('Ymd_His') . '.pdf';
return [
'content' => $dompdf->output(),
'filename' => $filename,
'width_mm' => $wMm,
'height_mm' => $hMm,
];
}
}
+191
View File
@@ -0,0 +1,191 @@
<?php
namespace App\Services\Reports;
use App\Models\LateSlipLog;
class SlipPrinterService
{
public function __construct(
private SlipPrinterConfigService $configService,
private SlipPrinterFormatterService $formatter,
private SlipPrinterPdfService $pdfService
) {
}
public function print(array $input, ?int $userId): array
{
$data = $this->buildData($input, $userId);
if ($data['student_name'] === '') {
return ['ok' => false, 'message' => 'Student name is required.'];
}
$this->logSlip($data, $userId);
$lines = $this->formatter->buildSlipLines($data, $this->lineWidth());
$pdf = $this->pdfService->render($data, $lines, $this->paperOptions($input));
return [
'ok' => true,
'data' => $data,
'pdf' => $pdf['content'],
'filename' => $pdf['filename'],
];
}
public function preview(array $input, ?int $userId): array
{
$data = $this->buildData($input, $userId);
$width = $this->lineWidth();
$feedLines = $this->configService->printerConfig()['feed_lines'] ?? 3;
$header = "Al Rahma School at ISGL\nSchool Year: {$data['school_year']}\n\n";
$body = implode("\n", $this->formatter->buildSlipLines($data, $width));
$footer = str_repeat("\n", (int) $feedLines);
return [
'ok' => true,
'text' => $header . $body . $footer,
'width' => $width,
];
}
public function logs(?string $schoolYear = null, ?string $semester = null): array
{
$query = LateSlipLog::query();
if ($schoolYear !== null && trim($schoolYear) !== '') {
$query->where('school_year', trim($schoolYear));
}
$semester = strtolower(trim((string) $semester));
if ($semester !== '') {
if ($semester === 'fall') {
$query->whereIn('semester', ['fall', '1', 1]);
} elseif ($semester === 'spring') {
$query->whereIn('semester', ['spring', '2', 2]);
}
}
$rows = $query->orderByDesc('id')->limit(50)->get()->toArray();
$out = [];
foreach ($rows as $row) {
$dispDate = $this->formatter->formatDisplayDate($row['slip_date'] ?? '');
if ($dispDate === '') {
$dispDate = $this->formatter->formatDisplayDate($row['printed_at'] ?? '');
}
$dispTime = $this->formatter->formatDisplayTime($row['time_in'] ?? '');
$out[] = [
'id' => (int) ($row['id'] ?? 0),
'school_year' => (string) ($row['school_year'] ?? ''),
'semester' => (string) ($row['semester'] ?? ''),
'student_name' => (string) ($row['student_name'] ?? ''),
'date' => $dispDate,
'time_in' => $dispTime,
'grade' => (string) ($row['grade'] ?? ''),
'reason' => (string) ($row['reason'] ?? ''),
'admin_name' => (string) ($row['admin_name'] ?? ''),
'printed_at' => (string) ($row['printed_at'] ?? ''),
];
}
return $out;
}
public function reprint(int $id, ?int $userId): array
{
$row = LateSlipLog::query()->find($id);
if (!$row) {
return ['ok' => false, 'message' => 'Slip not found.'];
}
$data = [
'school_year' => (string) ($row->school_year ?? ''),
'student_name' => (string) ($row->student_name ?? ''),
'date' => $this->formatter->formatDisplayDate($row->slip_date ?? '') ?: date('m/d/Y'),
'time_in' => $this->formatter->formatDisplayTime($row->time_in ?? '') ?: date('h:i A'),
'grade' => (string) ($row->grade ?? ''),
'reason' => (string) ($row->reason ?? ''),
'admin_name' => (string) ($row->admin_name ?? ''),
];
$adminFromDb = $this->configService->currentAdminName($userId);
if ($adminFromDb !== '') {
$data['admin_name'] = $adminFromDb;
}
$lines = $this->formatter->buildSlipLines($data, $this->lineWidth());
$pdf = $this->pdfService->render($data, $lines, []);
$this->logSlip($data, $userId);
return [
'ok' => true,
'data' => $data,
'pdf' => $pdf['content'],
'filename' => $pdf['filename'],
];
}
private function buildData(array $input, ?int $userId): array
{
$context = $this->configService->context();
$schoolYear = trim((string) ($input['school_year'] ?? $context['school_year'] ?? ''));
$data = [
'school_year' => $schoolYear,
'student_name' => trim((string) ($input['student_name'] ?? '')),
'date' => trim((string) ($input['date'] ?? date('m/d/Y'))),
'time_in' => trim((string) ($input['time_in'] ?? date('h:i A'))),
'grade' => trim((string) ($input['grade'] ?? '')),
'reason' => trim((string) ($input['reason'] ?? '')),
'admin_name' => trim((string) ($input['admin_name'] ?? '')),
];
$adminFromDb = $this->configService->currentAdminName($userId);
if ($adminFromDb !== '') {
$data['admin_name'] = $adminFromDb;
}
return $data;
}
private function logSlip(array $data, ?int $userId): void
{
$context = $this->configService->context();
$logData = [
'school_year' => $data['school_year'],
'semester' => (string) ($context['semester'] ?? ''),
'student_name' => $data['student_name'],
'slip_date' => $this->formatter->toDbDate($data['date']),
'time_in' => $this->formatter->toDbTime($data['time_in']),
'grade' => $data['grade'],
'reason' => $data['reason'],
'admin_name' => $data['admin_name'],
];
LateSlipLog::logSlip($logData, $userId);
}
private function lineWidth(): int
{
$cfg = $this->configService->printerConfig();
$width = (int) ($cfg['chars_per_line'] ?? 48);
return $width > 0 ? $width : 48;
}
private function paperOptions(array $input): array
{
return [
'paper' => $input['paper'] ?? null,
'font_pt' => $input['font_pt'] ?? null,
'line_h' => $input['line_h'] ?? null,
'rows' => $input['rows'] ?? null,
'height_mm' => $input['height_mm'] ?? null,
'h' => $input['h'] ?? null,
'fudge_mm' => $input['fudge_mm'] ?? null,
];
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Services\SchoolIds;
use App\Services\SchoolIdService;
class SchoolIdAssignmentService
{
public function __construct(private SchoolIdService $schoolIdService)
{
}
public function assignToUser(int $userId): ?string
{
return $this->schoolIdService->assignSchoolIdToUser($userId);
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Services\SchoolIds;
use App\Services\SchoolIdService;
class SchoolIdGenerationService
{
public function __construct(private SchoolIdService $schoolIdService)
{
}
public function generateStudentId(): string
{
return $this->schoolIdService->generateStudentSchoolId();
}
public function generateUserId(): string
{
return $this->schoolIdService->generateUserSchoolId();
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Services\Settings;
use App\Models\Configuration;
class ConfigurationService
{
public function list(): array
{
return Configuration::query()
->orderBy('id')
->get()
->toArray();
}
public function store(array $payload): Configuration
{
return Configuration::query()->create([
'config_key' => (string) $payload['config_key'],
'config_value' => (string) $payload['config_value'],
]);
}
public function update(int $id, array $payload): ?Configuration
{
$config = Configuration::query()->find($id);
if (!$config) {
return null;
}
$config->update([
'config_key' => (string) $payload['config_key'],
'config_value' => (string) $payload['config_value'],
]);
return $config;
}
public function delete(int $id): bool
{
$config = Configuration::query()->find($id);
if (!$config) {
return false;
}
return (bool) $config->delete();
}
}
@@ -0,0 +1,316 @@
<?php
namespace App\Services\Students;
use App\Models\ClassSection;
use App\Models\Enrollment;
use App\Models\Student;
use App\Models\StudentClass;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class StudentAssignmentService
{
public function __construct(private StudentConfigService $configService)
{
}
public function assignClasses(int $studentId, array $classSectionIds, bool $isEventOnly, ?int $userId = null): array
{
$context = $this->configService->context();
$semester = (string) ($context['semester'] ?? '');
$schoolYear = (string) ($context['school_year'] ?? '');
$classSectionIds = $this->normalizeIds($classSectionIds);
if ($studentId <= 0 || empty($classSectionIds) || $schoolYear === '') {
return ['ok' => false, 'message' => 'Missing required data (student/class section).'];
}
$student = Student::query()->find($studentId);
if (!$student) {
return ['ok' => false, 'message' => 'Student not found.'];
}
$sections = ClassSection::query()
->whereIn('class_section_id', $classSectionIds)
->orWhereIn('id', $classSectionIds)
->get()
->toArray();
if (empty($sections)) {
return ['ok' => false, 'message' => 'Class/Section not found.'];
}
$sectionMap = [];
foreach ($sections as $section) {
$sectionMap[(int) ($section['class_section_id'] ?? 0)] = $section;
}
$missing = array_diff($classSectionIds, array_keys($sectionMap));
if (!empty($missing)) {
return ['ok' => false, 'message' => 'One or more selected classes do not exist.'];
}
$existing = StudentClass::query()
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->get()
->keyBy(fn ($row) => (int) ($row->class_section_id ?? 0));
$primarySectionId = $classSectionIds[0];
$primarySection = $sectionMap[$primarySectionId] ?? reset($sectionMap);
$parentClassId = (int) ($primarySection['class_id'] ?? 0);
if ($parentClassId <= 0) {
$parentClassId = (int) (ClassSection::getClassId($primarySectionId) ?? 0);
}
$attendanceStats = [];
$scoreStats = [];
DB::transaction(function () use (
$studentId,
$schoolYear,
$classSectionIds,
$isEventOnly,
$userId,
$existing,
$semester,
$primarySectionId,
$parentClassId,
&$attendanceStats,
&$scoreStats
): void {
foreach ($classSectionIds as $classSectionId) {
$eventFlag = $isEventOnly ? 1 : 0;
if ($existing->has($classSectionId)) {
$eventFlag = (int) ($existing[$classSectionId]->is_event_only ?? 0);
}
$payload = [
'student_id' => $studentId,
'class_section_id' => $classSectionId,
'school_year' => $schoolYear,
'is_event_only' => $eventFlag,
'updated_by' => $userId,
'updated_at' => now(),
];
if ($existing->has($classSectionId)) {
$existing[$classSectionId]->fill($payload)->save();
} else {
StudentClass::query()->create($payload + [
'created_at' => now(),
]);
}
}
if (!$isEventOnly) {
$enrollment = Enrollment::query()
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->when($semester !== '', fn ($q) => $q->where('semester', $semester))
->first();
if ($enrollment) {
$enrollment->update([
'class_section_id' => $primarySectionId,
'enrollment_status' => 'payment pending',
'admission_status' => 'accepted',
'updated_at' => now(),
]);
}
$attendanceStats = $this->updateAttendance(
$studentId,
$primarySectionId,
$parentClassId,
$semester,
$schoolYear,
$userId
);
$scoreStats = $this->updateScores(
$studentId,
$primarySectionId,
$semester,
$schoolYear,
$userId
);
}
});
$displayNames = [];
foreach ($classSectionIds as $classSectionId) {
$section = $sectionMap[$classSectionId] ?? null;
if (!$section) {
continue;
}
$name = (string) ($section['class_section_name'] ?? '');
if ($name === '') {
$name = 'Section #' . $classSectionId;
}
$displayNames[] = $name;
}
return [
'ok' => true,
'student_id' => $studentId,
'class_section_id' => $primarySectionId,
'class_section_ids' => $classSectionIds,
'class_section_names' => $displayNames,
'attendance_updates' => $attendanceStats,
'score_updates' => $scoreStats,
];
}
public function removeClass(int $studentId, int $classSectionId, ?int $userId = null): array
{
$context = $this->configService->context();
$semester = (string) ($context['semester'] ?? '');
$schoolYear = (string) ($context['school_year'] ?? '');
if ($studentId <= 0 || $classSectionId <= 0 || $schoolYear === '') {
return ['ok' => false, 'message' => 'Missing required data (student/class section).'];
}
$row = StudentClass::query()
->where('student_id', $studentId)
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear)
->first();
if (!$row) {
return ['ok' => false, 'message' => 'Assignment not found for this student/class.'];
}
$remainingIds = [];
$remainingNames = [];
DB::transaction(function () use (
$studentId,
$classSectionId,
$schoolYear,
$semester,
$userId,
&$remainingIds,
&$remainingNames
): void {
StudentClass::query()
->where('student_id', $studentId)
->where('class_section_id', $classSectionId)
->where('school_year', $schoolYear)
->delete();
$remainingIds = StudentClass::getClassSectionIdsByStudentId($studentId, $schoolYear);
$remainingNames = StudentClass::getClassSectionsByStudentId($studentId, $schoolYear, true);
$enrollment = Enrollment::query()
->where('student_id', $studentId)
->where('school_year', $schoolYear)
->when($semester !== '', fn ($q) => $q->where('semester', $semester))
->first();
if ($enrollment) {
$newClassId = !empty($remainingIds) ? $remainingIds[0] : null;
if ((int) $enrollment->class_section_id === $classSectionId || $newClassId !== null) {
$enrollment->update([
'class_section_id' => $newClassId,
'updated_at' => now(),
'updated_by' => $userId,
]);
}
}
});
return [
'ok' => true,
'student_id' => $studentId,
'removed_class_id' => $classSectionId,
'remaining_ids' => $remainingIds,
'remaining_names' => $remainingNames,
];
}
private function updateAttendance(
int $studentId,
int $classSectionId,
int $classId,
string $semester,
string $schoolYear,
?int $modifiedBy
): array {
$now = now()->toDateTimeString();
$attendanceUpdated = DB::table('attendance_data')
->where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->update([
'class_section_id' => $classSectionId,
'class_id' => $classId,
'updated_at' => $now,
'modified_by' => $modifiedBy,
]);
$recordUpdated = DB::table('attendance_record')
->where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->update([
'class_section_id' => $classSectionId,
'updated_at' => $now,
'modified_by' => $modifiedBy,
]);
return [
'attendance_data_updated' => $attendanceUpdated,
'attendance_record_updated' => $recordUpdated,
];
}
private function updateScores(
int $studentId,
int $classSectionId,
string $semester,
string $schoolYear,
?int $modifiedBy
): array {
$now = now()->toDateTimeString();
$tables = [
'homework',
'quiz',
'project',
'participation',
'midterm_exam',
'final_exam',
'final_score',
'semester_scores',
];
$results = [];
foreach ($tables as $table) {
$payload = [
'class_section_id' => $classSectionId,
'updated_at' => $now,
];
if ($modifiedBy !== null && Schema::hasColumn($table, 'updated_by')) {
$payload['updated_by'] = $modifiedBy;
}
$results[$table . '_updated'] = DB::table($table)
->where('student_id', $studentId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->update($payload);
}
return $results;
}
private function normalizeIds(array $ids): array
{
$values = array_map('intval', $ids);
$values = array_values(array_unique(array_filter($values, static fn ($v) => $v > 0)));
return $values;
}
}
@@ -0,0 +1,256 @@
<?php
namespace App\Services\Students;
use App\Models\ClassSection;
use App\Models\PromotionQueue;
use App\Models\StudentClass;
use Illuminate\Support\Facades\DB;
class StudentAutoDistributionService
{
public function __construct(private StudentConfigService $configService)
{
}
public function promotionTotals(?string $schoolYear = null): array
{
$context = $this->configService->context();
$year = $schoolYear ?: (string) ($context['school_year'] ?? '');
$bases = ClassSection::query()
->whereRaw("class_section_name NOT LIKE '%-%'")
->orderBy('class_id')
->get()
->toArray();
$wanted = [];
foreach ($bases as $row) {
$nameRaw = (string) ($row['class_section_name'] ?? '');
$name = strtolower($nameRaw);
if ($name === 'kg' || $name === 'youth') {
$wanted[] = $row;
continue;
}
if (ctype_digit($name)) {
$num = (int) $name;
if ($num >= 1 && $num <= 9) {
$wanted[] = $row;
}
}
}
$rows = [];
foreach ($wanted as $row) {
$classId = (int) ($row['class_id'] ?? 0);
if ($classId <= 0) {
continue;
}
$cands = DB::table('promotion_queue as pq')
->select('pq.student_id')
->join('enrollments as e', function ($join) use ($year) {
$join->on('e.student_id', '=', 'pq.student_id')
->where('e.school_year', '=', $year);
})
->where('pq.to_class_id', $classId)
->where('pq.school_year_to', $year)
->whereIn('pq.status', ['queued', 'assigned', 'applied'])
->whereIn('e.enrollment_status', ['payment pending', 'enrolled'])
->groupBy('pq.student_id')
->get();
$rows[] = [
'class_id' => $classId,
'class_section_id' => (int) ($row['class_section_id'] ?? 0),
'class_section_name' => (string) ($row['class_section_name'] ?? ''),
'total' => $cands->count(),
];
}
return [
'year' => $year,
'rows' => $rows,
];
}
public function autoDistribute(int $classId, int $studentsPerSection, ?string $schoolYear = null, ?int $classSectionId = null, ?int $userId = null): array
{
$context = $this->configService->context();
$year = $schoolYear ?: (string) ($context['school_year'] ?? '');
$semester = (string) ($context['semester'] ?? '');
if ($classId <= 0 && $classSectionId > 0) {
$classId = (int) (ClassSection::getClassId($classSectionId) ?? 0);
}
if ($classId <= 0 || $studentsPerSection <= 0) {
return ['ok' => false, 'message' => 'Invalid class_id or students_per_section.'];
}
$cands = DB::table('promotion_queue as pq')
->select('pq.*', 'students.gender')
->join('students', 'students.id', '=', 'pq.student_id')
->where('pq.to_class_id', $classId)
->where('pq.school_year_to', $year)
->whereIn('pq.status', ['queued', 'assigned'])
->get()
->map(fn ($r) => (array) $r)
->all();
if (empty($cands)) {
return ['ok' => false, 'message' => 'No students found in promotion queue for selected class/year.'];
}
$studentIds = array_map(static fn ($r) => (int) $r['student_id'], $cands);
$enrolledIds = DB::table('enrollments')
->select('student_id')
->whereIn('student_id', $studentIds)
->where('school_year', $year)
->whereIn('enrollment_status', ['payment pending', 'enrolled'])
->groupBy('student_id')
->pluck('student_id')
->map(fn ($v) => (int) $v)
->all();
$cands = array_values(array_filter($cands, static function ($row) use ($enrolledIds) {
return in_array((int) $row['student_id'], $enrolledIds, true);
}));
if (empty($cands)) {
return ['ok' => false, 'message' => 'No eligible enrolled students found to distribute.'];
}
$sectionsNeeded = (int) ceil(count($cands) / $studentsPerSection);
$letters = ClassSection::getLetterSectionsByClassId($classId);
if (empty($letters)) {
return ['ok' => false, 'message' => 'No lettered sections found for the selected class.'];
}
if (count($letters) < $sectionsNeeded) {
return ['ok' => false, 'message' => 'Not enough sections available.'];
}
$letters = array_slice($letters, 0, $sectionsNeeded);
$buckets = [];
foreach ($letters as $idx => $section) {
$buckets[$idx] = [
'class_section_id' => (int) ($section['class_section_id'] ?? 0),
'assigned' => [],
'male' => 0,
'female' => 0,
];
}
$males = [];
$females = [];
foreach ($cands as $row) {
$gender = (string) ($row['gender'] ?? '');
if (strcasecmp($gender, 'Female') === 0) {
$females[] = $row;
} else {
$males[] = $row;
}
}
$pickBucket = function (string $gender) use (&$buckets, $studentsPerSection): ?int {
$bestIdx = null;
$bestCnt = PHP_INT_MAX;
foreach ($buckets as $idx => $bucket) {
if (count($bucket['assigned']) >= $studentsPerSection) {
continue;
}
$cnt = $gender === 'female' ? $bucket['female'] : $bucket['male'];
if ($cnt < $bestCnt) {
$bestCnt = $cnt;
$bestIdx = $idx;
}
}
return $bestIdx;
};
foreach ($males as $row) {
$idx = $pickBucket('male');
if ($idx === null) {
break;
}
$buckets[$idx]['assigned'][] = (int) $row['student_id'];
$buckets[$idx]['male']++;
}
foreach ($females as $row) {
$idx = $pickBucket('female');
if ($idx === null) {
break;
}
$buckets[$idx]['assigned'][] = (int) $row['student_id'];
$buckets[$idx]['female']++;
}
$promoIdsByStudent = [];
foreach ($cands as $row) {
$promoIdsByStudent[(int) $row['student_id']] = (int) $row['id'];
}
DB::transaction(function () use ($buckets, $promoIdsByStudent, $year, $semester, $userId): void {
foreach ($buckets as $bucket) {
$sectionId = (int) $bucket['class_section_id'];
foreach ($bucket['assigned'] as $studentId) {
if (isset($promoIdsByStudent[$studentId])) {
PromotionQueue::query()->where('id', $promoIdsByStudent[$studentId])->update([
'to_class_section_id' => $sectionId,
'status' => 'assigned',
'updated_by' => $userId,
'updated_at' => now(),
]);
}
$exists = StudentClass::query()
->where('student_id', $studentId)
->where('school_year', $year)
->when($semester !== '', fn ($q) => $q->where('semester', $semester))
->first();
$payload = [
'student_id' => $studentId,
'class_section_id' => $sectionId,
'school_year' => $year,
'semester' => $semester,
'updated_by' => $userId,
'updated_at' => now(),
];
if ($exists) {
$exists->update($payload);
} else {
StudentClass::query()->create($payload + ['created_at' => now()]);
}
}
}
});
$nameById = [];
foreach ($letters as $row) {
$nameById[(int) $row['class_section_id']] = (string) ($row['class_section_name'] ?? '');
}
$summary = [];
foreach ($buckets as $bucket) {
$sectionId = (int) $bucket['class_section_id'];
$summary[] = [
'class_section_id' => $sectionId,
'class_section_name' => $nameById[$sectionId] ?? (string) $sectionId,
'total' => count($bucket['assigned']),
'male' => $bucket['male'],
'female' => $bucket['female'],
];
}
return [
'ok' => true,
'message' => 'Auto distribution completed.',
'sections' => $summary,
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Services\Students;
use App\Models\Configuration;
class StudentConfigService
{
public function context(): array
{
return [
'school_year' => Configuration::getConfig('school_year'),
'semester' => Configuration::getConfig('semester'),
];
}
}
@@ -0,0 +1,164 @@
<?php
namespace App\Services\Students;
use App\Models\ClassSection;
use App\Models\EmergencyContact;
use App\Models\Student;
use App\Models\StudentClass;
use Illuminate\Support\Facades\DB;
class StudentDirectoryService
{
public function __construct(private StudentConfigService $configService)
{
}
public function assignmentData(?string $schoolYear = null): array
{
$context = $this->configService->context();
$schoolYear = $schoolYear ?: (string) ($context['school_year'] ?? '');
$semester = (string) ($context['semester'] ?? '');
$yearsRows = DB::table('enrollments')
->selectRaw('DISTINCT school_year')
->orderByDesc('school_year')
->get()
->map(fn ($r) => (array) $r)
->all();
$schoolYears = array_values(array_filter(array_map(static function ($r) {
return isset($r['school_year']) ? (string) $r['school_year'] : null;
}, $yearsRows)));
$students = Student::query()
->select('students.id', 'students.firstname', 'students.lastname', 'students.registration_date', 'students.is_new', 'students.age', 'students.parent_id', 'students.registration_grade')
->join('enrollments as e', 'e.student_id', '=', 'students.id')
->when($schoolYear !== '', fn ($q) => $q->where('e.school_year', $schoolYear))
->groupBy('students.id')
->orderBy('students.lastname')
->orderBy('students.firstname')
->get()
->toArray();
if (empty($students)) {
$students = Student::query()
->select('students.id', 'students.firstname', 'students.lastname', 'students.registration_date', 'students.is_new', 'students.age', 'students.parent_id', 'students.registration_grade')
->orderBy('students.lastname')
->orderBy('students.firstname')
->get()
->toArray();
}
$studentData = [];
foreach ($students as $student) {
$studentId = (int) ($student['id'] ?? 0);
$sectionNames = StudentClass::getClassSectionsByStudentIdWithFlags($studentId, $schoolYear, true);
$sectionIds = StudentClass::getClassSectionIdsByStudentId($studentId, $schoolYear);
$sectionDisplay = !empty($sectionNames) ? implode(', ', $sectionNames) : '';
$parentId = (int) ($student['parent_id'] ?? 0);
$emergencyInfo = $parentId > 0
? (EmergencyContact::getEmergencyContactByParentId($parentId) ?? [])
: [];
$studentData[] = [
'student_id' => $studentId,
'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
'age' => $student['age'] ?? 'N/A',
'email' => $emergencyInfo['emergency_contact_name'] ?? '',
'phone' => $emergencyInfo['cellphone'] ?? '',
'registration_grade' => $student['registration_grade'] ?? 'N/A',
'class_section_name' => $sectionDisplay !== '' ? $sectionDisplay : 'No class assigned',
'class_section_names' => $sectionNames,
'class_section_ids' => $sectionIds,
'new_student' => ((int) ($student['is_new'] ?? 0) === 1) ? 'Yes' : 'No',
'registration_date' => $student['registration_date'] ?? '',
'school_year' => $schoolYear,
'semester' => $semester,
];
}
$classes = ClassSection::query()
->select('id', 'class_section_id', 'class_section_name', 'school_year')
->when($schoolYear !== '', fn ($q) => $q->where('school_year', $schoolYear))
->orderBy('class_section_name')
->get()
->toArray();
if (empty($classes)) {
$classes = ClassSection::query()
->select('id', 'class_section_id', 'class_section_name')
->orderBy('class_section_name')
->get()
->toArray();
}
return [
'students' => $studentData,
'classes' => $classes,
'schoolYears' => $schoolYears,
'selectedYear' => $schoolYear,
'currentYear' => (string) ($context['school_year'] ?? ''),
'isCurrentYear' => $schoolYear !== '' && $schoolYear === (string) ($context['school_year'] ?? ''),
];
}
public function removedStudents(?string $schoolYear = null): array
{
$context = $this->configService->context();
$schoolYear = $schoolYear ?: (string) ($context['school_year'] ?? '');
$activeStudents = Student::query()
->select('id', 'school_id', 'firstname', 'lastname', 'gender', 'age')
->where('is_active', 1)
->orderBy('lastname')
->orderBy('firstname')
->get()
->toArray();
$removedStudents = Student::query()
->select('id', 'school_id', 'firstname', 'lastname', 'gender', 'age')
->where('is_active', 0)
->orderBy('lastname')
->orderBy('firstname')
->get()
->toArray();
$classRows = DB::table('student_class as sc')
->select('sc.student_id', 'cs.class_section_name')
->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
->when($schoolYear !== '', fn ($q) => $q->where('sc.school_year', $schoolYear))
->get()
->map(fn ($r) => (array) $r)
->all();
$classMap = [];
foreach ($classRows as $row) {
$studentId = (int) ($row['student_id'] ?? 0);
$name = trim((string) ($row['class_section_name'] ?? ''));
if ($studentId <= 0 || $name === '') {
continue;
}
$classMap[$studentId][] = $name;
}
$attachClasses = static function (array $students) use ($classMap): array {
foreach ($students as &$student) {
$studentId = (int) ($student['id'] ?? 0);
$names = $classMap[$studentId] ?? [];
$names = array_values(array_unique(array_filter($names)));
$student['class_sections'] = $names;
$student['class_section_name'] = !empty($names) ? implode(', ', $names) : 'No class assigned';
}
unset($student);
return $students;
};
return [
'active_students' => $attachClasses($activeStudents),
'removed_students' => $attachClasses($removedStudents),
'school_year' => $schoolYear,
];
}
}
@@ -0,0 +1,135 @@
<?php
namespace App\Services\Students;
use App\Models\Student;
use App\Models\StudentAllergy;
use App\Models\StudentMedicalCondition;
use Illuminate\Support\Facades\DB;
class StudentProfileService
{
public function updateStudent(int $studentId, array $payload): array
{
$student = Student::query()->find($studentId);
if (!$student) {
return ['ok' => false, 'message' => 'Student not found.'];
}
$studentData = array_filter([
'school_id' => $payload['school_id'] ?? null,
'firstname' => $payload['firstname'] ?? null,
'lastname' => $payload['lastname'] ?? null,
'dob' => $payload['dob'] ?? null,
'age' => $payload['age'] ?? null,
'gender' => $payload['gender'] ?? null,
'registration_grade' => $payload['registration_grade'] ?? null,
'photo_consent' => $payload['photo_consent'] ?? null,
'parent_id' => $payload['parent_id'] ?? null,
'registration_date' => $payload['registration_date'] ?? null,
'tuition_paid' => $payload['tuition_paid'] ?? null,
'year_of_registration' => $payload['year_of_registration'] ?? null,
'school_year' => $payload['school_year'] ?? null,
'rfid_tag' => $payload['rfid_tag'] ?? null,
'semester' => $payload['semester'] ?? null,
'is_new' => $payload['is_new'] ?? null,
'is_active' => $payload['is_active'] ?? null,
], static fn ($value) => $value !== null);
$conditionsInput = (string) ($payload['medical_conditions'] ?? '');
$allergiesInput = (string) ($payload['allergies'] ?? '');
$conditions = $this->normalizeHealthList($conditionsInput);
$allergies = $this->normalizeHealthList($allergiesInput);
$shouldSyncConditions = array_key_exists('medical_conditions', $payload);
$shouldSyncAllergies = array_key_exists('allergies', $payload);
DB::transaction(function () use ($student, $studentData, $conditions, $allergies): void {
if (!empty($studentData)) {
$student->update($studentData);
}
if ($shouldSyncConditions) {
$this->syncHealthList($student->id, StudentMedicalCondition::class, 'condition_name', $conditions);
}
if ($shouldSyncAllergies) {
$this->syncHealthList($student->id, StudentAllergy::class, 'allergy', $allergies);
}
});
return ['ok' => true, 'message' => 'Student updated successfully.'];
}
private function normalizeHealthList(string $value, int $maxLen = 100): array
{
$value = trim($value);
if ($value === '') {
return [];
}
$parts = preg_split('/[,\n;]+/u', $value, -1, PREG_SPLIT_NO_EMPTY) ?: [];
$out = [];
foreach ($parts as $part) {
$item = trim(preg_replace('/\s+/u', ' ', $part));
if ($item === '') {
continue;
}
if (preg_match('/^(none|n\/a|na|null|nil|no)$/i', $item)) {
continue;
}
$out[mb_strtolower($item, 'UTF-8')] = mb_substr($item, 0, $maxLen, 'UTF-8');
}
return array_values($out);
}
private function syncHealthList(int $studentId, string $modelClass, string $field, array $newValues): void
{
$model = new $modelClass();
$rows = $model->newQuery()->where('student_id', $studentId)->get(['id', $field])->toArray();
$current = [];
$byId = [];
foreach ($rows as $row) {
$val = (string) ($row[$field] ?? '');
$key = mb_strtolower(trim($val), 'UTF-8');
if ($key === '') {
continue;
}
$current[$key] = $val;
$byId[$key] = (int) ($row['id'] ?? 0);
}
$incoming = [];
foreach ($newValues as $value) {
$key = mb_strtolower(trim($value), 'UTF-8');
if ($key === '') {
continue;
}
$incoming[$key] = $value;
}
$toDeleteKeys = array_diff(array_keys($current), array_keys($incoming));
$toInsertKeys = array_diff(array_keys($incoming), array_keys($current));
if (!empty($toDeleteKeys)) {
$ids = array_map(fn ($key) => $byId[$key], $toDeleteKeys);
if (!empty($ids)) {
$model->newQuery()->whereIn('id', $ids)->delete();
}
}
if (!empty($toInsertKeys)) {
$batch = [];
foreach ($toInsertKeys as $key) {
$batch[] = [
'student_id' => $studentId,
$field => $incoming[$key],
];
}
if (!empty($batch)) {
$model->newQuery()->insert($batch);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More