add controllers, servoices

This commit is contained in:
root
2026-03-09 02:52:13 -04:00
parent c8de5f7edc
commit d76c871cb7
501 changed files with 34439 additions and 21843 deletions
Binary file not shown.
@@ -8,7 +8,7 @@ use App\Http\Requests\AttendanceTracking\ParentsInfoRequest;
use App\Http\Requests\AttendanceTracking\RecordAttendanceTrackingRequest;
use App\Http\Requests\AttendanceTracking\SaveAttendanceNotificationNoteRequest;
use App\Http\Requests\AttendanceTracking\SendAttendanceManualEmailRequest;
use App\Services\AttendanceTrackingService;
use App\Services\AttendanceTracking\AttendanceTrackingService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Auth\RegisterRequest;
use App\Http\Resources\Auth\RegisterResource;
use App\Services\Auth\RegistrationCaptchaService;
use App\Services\Auth\RegistrationService;
use Illuminate\Http\JsonResponse;
class RegisterController extends BaseApiController
{
public function __construct(
private RegistrationService $service,
private RegistrationCaptchaService $captchaService
) {
parent::__construct();
}
public function captcha(): JsonResponse
{
$captcha = $this->captchaService->generate();
return response()->json([
'ok' => true,
'captcha' => $captcha,
]);
}
public function store(RegisterRequest $request): JsonResponse
{
if (app()->runningUnitTests() && $request->header('X-Debug-Request') === '1') {
return response()->json([
'ok' => true,
'debug' => [
'content_type' => $request->header('Content-Type'),
'content_length' => strlen((string) $request->getContent()),
'request_all' => $request->all(),
'json_all' => $request->json()->all(),
'payload_data' => $this->payloadData(),
],
]);
}
$result = $this->service->register($request->validated());
if (empty($result['ok'])) {
$code = $result['code'] ?? 'registration_failed';
$status = $code === 'pending_activation' || $code === 'email_exists' ? 409 : 422;
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Registration failed.',
'code' => $code,
], $status);
}
return response()->json([
'ok' => true,
'registration' => new RegisterResource($result),
], 201);
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Api\ClassPrep;
use App\Http\Controllers\Api\BaseApiController;
use App\Services\ClassPrep\ClassRosterService;
use App\Services\ClassPrep\StickerCountService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ClassPrepController extends BaseApiController
{
private StickerCountService $stickerCounts;
private ClassRosterService $roster;
public function __construct(StickerCountService $stickerCounts, ClassRosterService $roster)
{
parent::__construct();
$this->stickerCounts = $stickerCounts;
$this->roster = $roster;
}
public function stickerCounts(Request $request): JsonResponse
{
$schoolYear = (string) ($request->query('school_year') ?? '');
$semester = (string) ($request->query('semester') ?? '');
$classSectionId = $request->query('class_section_id') ?? $request->query('class_id');
$classSectionId = is_numeric($classSectionId) ? (int) $classSectionId : null;
if ($classSectionId !== null && $classSectionId > 0) {
$payload = $this->stickerCounts->listForClass($schoolYear, $semester, $classSectionId);
} else {
$payload = $this->stickerCounts->listAll($schoolYear, $semester);
}
return response()->json([
'ok' => true,
'data' => $payload,
]);
}
public function studentsByClass(Request $request, int $classSectionId): JsonResponse
{
$schoolYear = (string) ($request->query('school_year') ?? '');
$students = $this->roster->listStudentsByClass($classSectionId, $schoolYear !== '' ? $schoolYear : null);
return response()->json([
'ok' => true,
'students' => $students,
]);
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Api\ClassPreparation;
use App\Http\Controllers\Api\BaseApiController;
use App\Services\ClassPreparation\ClassPreparationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ClassPreparationController extends BaseApiController
{
private ClassPreparationService $service;
public function __construct(ClassPreparationService $service)
{
parent::__construct();
$this->service = $service;
}
public function index(Request $request): JsonResponse
{
$schoolYear = (string) ($request->query('school_year') ?? '');
$semester = (string) ($request->query('semester') ?? '');
$payload = $this->service->listPrep(
$schoolYear !== '' ? $schoolYear : null,
$semester !== '' ? $semester : null
);
return response()->json([
'ok' => true,
'schoolYear' => $payload['schoolYear'],
'semester' => $payload['semester'],
'results' => $payload['results'],
'totals' => $payload['totals'],
'shortages' => $payload['shortages'],
]);
}
public function markPrinted(Request $request): JsonResponse
{
$schoolYear = (string) ($request->input('school_year') ?? '');
$semester = (string) ($request->input('semester') ?? '');
$ids = $request->input('class_section_ids', []);
if ($schoolYear === '' || !is_array($ids)) {
return response()->json([
'ok' => false,
'message' => 'school_year and class_section_ids are required.',
], 422);
}
$count = $this->service->markPrinted($schoolYear, $semester, $ids);
return response()->json([
'ok' => true,
'updated' => $count,
]);
}
}
@@ -0,0 +1,178 @@
<?php
namespace App\Http\Controllers\Api\Communication;
use App\Http\Controllers\Api\BaseApiController;
use App\Services\Communication\CommunicationFamilyService;
use App\Services\Communication\CommunicationPreviewService;
use App\Services\Communication\CommunicationSendService;
use App\Services\Communication\CommunicationStudentService;
use App\Services\Communication\CommunicationTemplateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CommunicationController extends BaseApiController
{
private CommunicationStudentService $students;
private CommunicationTemplateService $templates;
private CommunicationFamilyService $families;
private CommunicationPreviewService $previewService;
private CommunicationSendService $sendService;
public function __construct(
CommunicationStudentService $students,
CommunicationTemplateService $templates,
CommunicationFamilyService $families,
CommunicationPreviewService $previewService,
CommunicationSendService $sendService
) {
parent::__construct();
$this->students = $students;
$this->templates = $templates;
$this->families = $families;
$this->previewService = $previewService;
$this->sendService = $sendService;
}
public function options(): JsonResponse
{
return response()->json([
'ok' => true,
'students' => $this->students->listStudents(),
'templates' => $this->templates->listActiveTemplates(),
]);
}
public function families(int $studentId): JsonResponse
{
return response()->json([
'ok' => true,
'data' => $this->families->familiesForStudent($studentId),
]);
}
public function guardians(int $familyId): JsonResponse
{
return response()->json([
'ok' => true,
'data' => $this->families->guardiansForFamily($familyId),
]);
}
public function preview(Request $request): JsonResponse
{
$templateKey = (string) $request->input('template_key', '');
$studentId = (int) $request->input('student_id', 0);
$familyId = (int) $request->input('family_id', 0);
$vars = $this->normalizeArray($request->input('vars', []));
if ($templateKey === '' || $studentId <= 0 || $familyId <= 0) {
return response()->json([
'ok' => false,
'message' => 'template_key, student_id, and family_id are required.',
], 422);
}
$teacherName = $this->resolveTeacherName();
$result = $this->previewService->buildPreview($templateKey, $studentId, $familyId, $vars, $teacherName);
if (!$result['ok']) {
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Preview failed.',
], $result['status'] ?? 400);
}
return response()->json([
'ok' => true,
'subject' => $result['subject'],
'html' => $result['html'],
]);
}
public function send(Request $request): JsonResponse
{
$studentId = (int) $request->input('student_id', 0);
$familyId = (int) $request->input('family_id', 0);
$templateKey = (string) $request->input('template_key', '');
$subject = (string) $request->input('subject', '');
$body = (string) $request->input('body', '');
$recipients = $this->normalizeArray($request->input('recipients', []));
$cc = $this->normalizeArray($request->input('cc', []));
$bcc = $this->normalizeArray($request->input('bcc', []));
if ($studentId <= 0 || $familyId <= 0 || $templateKey === '' || $subject === '' || $body === '') {
return response()->json([
'ok' => false,
'message' => 'student_id, family_id, template_key, subject, and body are required.',
], 422);
}
if (empty($recipients)) {
return response()->json([
'ok' => false,
'message' => 'At least one recipient is required.',
], 422);
}
$student = $this->students->getStudentBasic($studentId);
if (!$student) {
return response()->json([
'ok' => false,
'message' => 'Student not found.',
], 404);
}
$result = $this->sendService->send([
'student_id' => $studentId,
'family_id' => $familyId,
'template_key' => $templateKey,
'subject' => $subject,
'body' => $body,
'recipients' => $recipients,
'cc' => $cc,
'bcc' => $bcc,
'student_name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
'sent_by' => (int) (auth()->id() ?? 0),
]);
if (!$result['ok']) {
return response()->json([
'ok' => false,
'message' => 'Failed to send email.',
'error' => $result['error'],
], 500);
}
return response()->json([
'ok' => true,
'message' => 'Email sent successfully.',
]);
}
private function normalizeArray($value): array
{
if (is_array($value)) {
return $value;
}
if (is_string($value) && $value !== '') {
$decoded = json_decode($value, true);
if (is_array($decoded)) {
return $decoded;
}
}
return [];
}
private function resolveTeacherName(): string
{
$user = auth()->user();
if ($user) {
$name = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? ''));
return $name !== '' ? $name : 'Teacher';
}
return 'Teacher';
}
}
@@ -0,0 +1,212 @@
<?php
namespace App\Http\Controllers\Api\CompetitionScores;
use App\Http\Controllers\Api\BaseApiController;
use App\Models\Competition;
use App\Services\CompetitionScores\CompetitionScoresContextService;
use App\Services\CompetitionScores\CompetitionScoresQueryService;
use App\Services\CompetitionScores\CompetitionScoresRosterService;
use App\Services\CompetitionScores\CompetitionScoresSaveService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CompetitionScoresController extends BaseApiController
{
private CompetitionScoresContextService $contextService;
private CompetitionScoresQueryService $queryService;
private CompetitionScoresRosterService $rosterService;
private CompetitionScoresSaveService $saveService;
public function __construct(
CompetitionScoresContextService $contextService,
CompetitionScoresQueryService $queryService,
CompetitionScoresRosterService $rosterService,
CompetitionScoresSaveService $saveService
) {
parent::__construct();
$this->contextService = $contextService;
$this->queryService = $queryService;
$this->rosterService = $rosterService;
$this->saveService = $saveService;
}
public function index(Request $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
[$assignments, $schoolYear, $semester] = $this->contextService->getTeacherContext($userId);
$activeClassId = $this->contextService->resolveActiveClassId(
$assignments,
(int) $request->query('class_section_id', 0)
);
$activeClassName = $this->contextService->getActiveClassName($assignments, $activeClassId);
if (empty($assignments) || $activeClassId <= 0) {
return response()->json([
'ok' => true,
'competitions' => [],
'activeClassId' => $activeClassId,
'activeClassName' => $activeClassName,
'questionCounts' => [],
'scoreCounts' => [],
'studentTotal' => 0,
'sectionMap' => $this->contextService->getClassSectionMap(),
'schoolYear' => $schoolYear,
'semester' => $semester,
'hasClasses' => false,
]);
}
$competitions = $this->queryService->getCompetitionsForClass($activeClassId, $schoolYear, $semester);
$competitionIds = array_values(array_filter(array_map(static function ($row) {
return (int) ($row['id'] ?? 0);
}, $competitions)));
return response()->json([
'ok' => true,
'competitions' => $competitions,
'activeClassId' => $activeClassId,
'activeClassName' => $activeClassName,
'questionCounts' => $this->queryService->getQuestionCounts($competitionIds, $activeClassId),
'scoreCounts' => $this->queryService->getScoreCounts($competitionIds, $activeClassId),
'studentTotal' => $this->rosterService->getClassStudentCount($activeClassId, $schoolYear),
'sectionMap' => $this->contextService->getClassSectionMap(),
'schoolYear' => $schoolYear,
'semester' => $semester,
'hasClasses' => true,
]);
}
public function edit(Request $request, int $id): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
[$assignments, $schoolYear, $semester] = $this->contextService->getTeacherContext($userId);
$allowedClassIds = $this->contextService->getAllowedClassIds($assignments);
if (empty($allowedClassIds)) {
return response()->json([
'ok' => false,
'message' => 'No class assignments found for your account.',
], 403);
}
$competition = Competition::query()->find($id);
if (!$competition) {
return response()->json([
'ok' => false,
'message' => 'Competition not found.',
], 404);
}
$isLocked = (bool) $competition->is_locked;
$lockedClassId = (int) ($competition->class_section_id ?? 0);
$requestedClassId = (int) $request->query('class_section_id', 0);
$activeClassId = $this->contextService->resolveActiveClassId($assignments, $requestedClassId);
$classSectionId = $lockedClassId > 0 ? $lockedClassId : $activeClassId;
if ($classSectionId <= 0) {
return response()->json([
'ok' => false,
'message' => 'Select a class section before entering scores.',
], 422);
}
if (!in_array($classSectionId, $allowedClassIds, true)) {
return response()->json([
'ok' => false,
'message' => 'You are not assigned to that class.',
], 403);
}
$competitionArray = $competition->toArray();
$students = $this->rosterService->getStudentsForCompetition($competitionArray, $classSectionId);
$scoreMap = $this->saveService->getScoreMap($id, $classSectionId);
$questionCount = $this->queryService->getQuestionCountForCompetition($id, $classSectionId);
$classStudentCount = $this->rosterService->getClassStudentCount(
$classSectionId,
$competitionArray['school_year'] ?? $schoolYear
);
return response()->json([
'ok' => true,
'competition' => $competitionArray,
'students' => $students,
'scoreMap' => $scoreMap,
'classSectionId' => $classSectionId,
'classSectionName' => $this->contextService->getActiveClassName($assignments, $classSectionId),
'classStudentCount' => $classStudentCount,
'questionCount' => $questionCount,
'classSelectionLocked' => $lockedClassId > 0,
'isLocked' => $isLocked,
'schoolYear' => $schoolYear,
'semester' => $semester,
]);
}
public function save(Request $request, int $id): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
[$assignments] = $this->contextService->getTeacherContext($userId);
$allowedClassIds = $this->contextService->getAllowedClassIds($assignments);
if (empty($allowedClassIds)) {
return response()->json([
'ok' => false,
'message' => 'No class assignments found for your account.',
], 403);
}
$competition = Competition::query()->find($id);
if (!$competition) {
return response()->json([
'ok' => false,
'message' => 'Competition not found.',
], 404);
}
if ((bool) $competition->is_locked) {
return response()->json([
'ok' => false,
'message' => 'Competition is locked. You cannot update scores.',
], 423);
}
$lockedClassId = (int) ($competition->class_section_id ?? 0);
$classSectionId = $lockedClassId > 0 ? $lockedClassId : (int) $request->input('class_section_id', 0);
if ($classSectionId <= 0) {
return response()->json([
'ok' => false,
'message' => 'Select a class section before saving scores.',
], 422);
}
if (!in_array($classSectionId, $allowedClassIds, true)) {
return response()->json([
'ok' => false,
'message' => 'You are not assigned to that class.',
], 403);
}
$scores = $request->input('scores', []);
$scores = is_array($scores) ? $scores : [];
[$cleanScores, $invalidScores] = $this->saveService->filterScores($scores);
if (!empty($invalidScores)) {
return response()->json([
'ok' => false,
'message' => 'Scores must be whole numbers (no decimals).',
'invalidScores' => $invalidScores,
], 422);
}
$this->saveService->saveScores($id, $classSectionId, $cleanScores);
return response()->json([
'ok' => true,
'message' => 'Scores saved.',
'savedCount' => count($cleanScores),
]);
}
}
@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Api\Discounts;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Discounts\ApplyDiscountVoucherRequest;
use App\Http\Requests\Discounts\StoreDiscountVoucherRequest;
use App\Http\Requests\Discounts\UpdateDiscountVoucherRequest;
use App\Http\Resources\Discounts\DiscountParentResource;
use App\Http\Resources\Discounts\DiscountVoucherResource;
use App\Services\Discounts\DiscountApplyService;
use App\Services\Discounts\DiscountContextService;
use App\Services\Discounts\DiscountParentService;
use App\Services\Discounts\DiscountVoucherService;
use Illuminate\Http\JsonResponse;
class DiscountController extends BaseApiController
{
private DiscountContextService $context;
private DiscountVoucherService $voucherService;
private DiscountParentService $parentService;
private DiscountApplyService $applyService;
public function __construct(
DiscountContextService $context,
DiscountVoucherService $voucherService,
DiscountParentService $parentService,
DiscountApplyService $applyService
) {
parent::__construct();
$this->context = $context;
$this->voucherService = $voucherService;
$this->parentService = $parentService;
$this->applyService = $applyService;
}
public function options(): JsonResponse
{
$schoolYear = $this->context->getSchoolYear();
$vouchers = $this->voucherService->listActive();
$parents = $this->parentService->listParentsWithDiscounts($schoolYear);
return response()->json([
'ok' => true,
'vouchers' => DiscountVoucherResource::collection($vouchers),
'parents' => DiscountParentResource::collection($parents),
'schoolYear' => $schoolYear,
]);
}
public function apply(ApplyDiscountVoucherRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->applyService->applyVoucher(
(int) $payload['voucher_id'],
$payload['parent_ids'],
(bool) ($payload['allow_additional'] ?? false),
(int) (auth()->id() ?? 0)
);
return response()->json($result, $result['ok'] ? 200 : 422);
}
public function listVouchers(): JsonResponse
{
$vouchers = $this->voucherService->listAll();
return response()->json([
'ok' => true,
'vouchers' => DiscountVoucherResource::collection($vouchers),
]);
}
public function storeVoucher(StoreDiscountVoucherRequest $request): JsonResponse
{
$voucher = $this->voucherService->create($request->validated());
return response()->json([
'ok' => true,
'voucher' => new DiscountVoucherResource($voucher),
], 201);
}
public function updateVoucher(UpdateDiscountVoucherRequest $request, int $id): JsonResponse
{
$voucher = $this->voucherService->update($id, $request->validated());
return response()->json([
'ok' => true,
'voucher' => new DiscountVoucherResource($voucher),
]);
}
public function showVoucher(int $id): JsonResponse
{
$voucher = $this->voucherService->find($id);
return response()->json([
'ok' => true,
'voucher' => new DiscountVoucherResource($voucher),
]);
}
public function deleteVoucher(int $id): JsonResponse
{
$this->voucherService->delete($id);
return response()->json([
'ok' => true,
]);
}
}
@@ -0,0 +1,184 @@
<?php
namespace App\Http\Controllers\Api\Email;
use App\Http\Controllers\Api\BaseApiController;
use App\Services\BroadcastEmail\BroadcastEmailComposerService;
use App\Services\BroadcastEmail\BroadcastEmailDispatchService;
use App\Services\BroadcastEmail\BroadcastEmailImageService;
use App\Services\BroadcastEmail\BroadcastEmailRecipientService;
use App\Services\BroadcastEmail\BroadcastEmailSenderOptionsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BroadcastEmailController extends BaseApiController
{
private BroadcastEmailSenderOptionsService $senderOptions;
private BroadcastEmailRecipientService $recipients;
private BroadcastEmailComposerService $composer;
private BroadcastEmailDispatchService $dispatch;
private BroadcastEmailImageService $imageService;
public function __construct(
BroadcastEmailSenderOptionsService $senderOptions,
BroadcastEmailRecipientService $recipients,
BroadcastEmailComposerService $composer,
BroadcastEmailDispatchService $dispatch,
BroadcastEmailImageService $imageService
) {
parent::__construct();
$this->senderOptions = $senderOptions;
$this->recipients = $recipients;
$this->composer = $composer;
$this->dispatch = $dispatch;
$this->imageService = $imageService;
}
public function options(): JsonResponse
{
return response()->json([
'ok' => true,
'parents' => $this->recipients->parentsWithEmails(),
'fromOptions' => $this->senderOptions->listOptions(),
]);
}
public function send(Request $request): JsonResponse
{
$mode = (string) ($request->input('mode') ?? 'standard');
$subject = trim((string) ($request->input('subject') ?? ''));
$fromKey = trim((string) ($request->input('from_key') ?? 'general'));
$body = (string) ($request->input('body_html') ?? '');
$wrapLayout = (bool) $request->boolean('wrap_layout');
$preheader = (string) ($request->input('preheader') ?? '');
$ctaText = (string) ($request->input('cta_text') ?? '');
$ctaUrl = (string) ($request->input('cta_url') ?? '');
$testEmail = trim((string) ($request->input('test_email') ?? ''));
$isTestOnly = (bool) $request->boolean('send_test_only');
$body = $this->composer->sanitizeHtml($body);
if ($subject === '' || $body === '') {
return response()->json([
'ok' => false,
'message' => 'Subject and Body are required.',
], 422);
}
$personalize = $mode === 'personalized';
if ($isTestOnly) {
if ($testEmail === '') {
return response()->json([
'ok' => false,
'message' => 'Provide a test email address.',
], 422);
}
$result = $this->dispatch->sendTest([
'wrap_layout' => $wrapLayout,
'subject' => $subject,
'body_html' => $body,
'recipient_name' => 'Parent',
'preheader' => $preheader,
'cta_text' => $ctaText,
'cta_url' => $ctaUrl,
'personalize' => $personalize,
'test_email' => $testEmail,
'from_key' => $fromKey,
]);
return response()->json($result, $result['ok'] ? 200 : 500);
}
$rawIds = $request->input('parent_ids', []);
$ids = $this->normalizeIds($rawIds);
if (empty($ids)) {
return response()->json([
'ok' => false,
'message' => 'Please select at least one parent.',
], 422);
}
$recipients = $this->recipients->recipientsByIds($ids);
if (empty($recipients)) {
return response()->json([
'ok' => false,
'message' => 'No valid parent emails found.',
], 422);
}
$stats = $this->dispatch->sendBroadcast([
'wrap_layout' => $wrapLayout,
'subject' => $subject,
'body_html' => $body,
'preheader' => $preheader,
'cta_text' => $ctaText,
'cta_url' => $ctaUrl,
'personalize' => $personalize,
'from_key' => $fromKey,
'mode' => $mode,
], $recipients);
$message = sprintf(
'Broadcast finished. Mode: %s. Sent: %d/%d. Failures: %d.',
$stats['mode'],
$stats['sent'],
$stats['attempted'],
$stats['failed']
);
return response()->json([
'ok' => $stats['failed'] === 0,
'stats' => $stats,
'message' => $message,
]);
}
public function uploadImage(Request $request): JsonResponse
{
$file = $request->file('image');
if (!$file || !$file->isValid()) {
return response()->json([
'success' => false,
'error' => 'No image uploaded',
], 400);
}
$result = $this->imageService->store($file);
if (!$result['ok']) {
return response()->json([
'success' => false,
'error' => $result['error'] ?? 'Upload failed',
], $result['status'] ?? 400);
}
$newHash = function_exists('csrf_hash') ? csrf_hash() : csrf_token();
return response()->json([
'success' => true,
'url' => $result['url'],
'csrf_hash' => $newHash,
])->header('X-CSRF-HASH', (string) $newHash);
}
private function normalizeIds($rawIds): array
{
$ids = [];
$raw = is_array($rawIds) ? $rawIds : [$rawIds];
foreach ($raw as $value) {
if (is_string($value) && strpos($value, ',') !== false) {
foreach (explode(',', $value) as $piece) {
$ids[] = (int) $piece;
}
continue;
}
$ids[] = (int) $value;
}
return array_values(array_unique(array_filter($ids)));
}
}
@@ -0,0 +1,180 @@
<?php
namespace App\Http\Controllers\Api\Expenses;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Expenses\StoreExpenseRequest;
use App\Http\Requests\Expenses\UpdateExpenseRequest;
use App\Http\Requests\Expenses\UpdateExpenseStatusRequest;
use App\Http\Resources\Expenses\ExpenseResource;
use App\Http\Resources\Expenses\ExpenseStaffResource;
use App\Models\Expense;
use App\Services\Expenses\ExpenseContextService;
use App\Services\Expenses\ExpenseQueryService;
use App\Services\Expenses\ExpenseReceiptService;
use App\Services\Expenses\ExpenseRetailorService;
use App\Services\Expenses\ExpenseStaffService;
use App\Services\Expenses\ExpenseStatusService;
use App\Services\Expenses\ExpenseWriteService;
use Illuminate\Http\JsonResponse;
class ExpenseController extends BaseApiController
{
public function __construct(
private ExpenseContextService $context,
private ExpenseRetailorService $retailors,
private ExpenseStaffService $staff,
private ExpenseQueryService $queryService,
private ExpenseReceiptService $receiptService,
private ExpenseWriteService $writeService,
private ExpenseStatusService $statusService
) {
parent::__construct();
}
public function options(): JsonResponse
{
$schoolYear = $this->context->getSchoolYear();
$semester = $this->context->getSemester();
return response()->json([
'ok' => true,
'schoolYear' => $schoolYear,
'semester' => $semester,
'retailors' => $this->retailors->listRetailors(),
'staff' => ExpenseStaffResource::collection($this->staff->listStaffUsers()),
]);
}
public function index(): JsonResponse
{
$rows = $this->queryService->listAll();
$rows = array_map(function ($row) {
$row['receipt_url'] = $this->receiptService->receiptUrl($row['receipt_path'] ?? null);
return $row;
}, $rows);
return response()->json([
'ok' => true,
'expenses' => ExpenseResource::collection($rows),
]);
}
public function show(int $id): JsonResponse
{
$row = $this->queryService->findById($id);
if (!$row) {
return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404);
}
$row['receipt_url'] = $this->receiptService->receiptUrl($row['receipt_path'] ?? null);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($row),
]);
}
public function store(StoreExpenseRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = null;
if ($request->hasFile('receipt')) {
$receiptName = $this->receiptService->storeReceipt($request->file('receipt'));
}
$isDonation = $payload['category'] === 'Donation';
$expense = $this->writeService->store([
'category' => $payload['category'],
'amount' => $payload['amount'],
'receipt_path' => $receiptName,
'description' => $payload['description'] ?? null,
'retailor' => $payload['retailor'] ?? null,
'date_of_purchase' => $payload['date_of_purchase'],
'purchased_by' => (int) $payload['purchased_by'],
'added_by' => $userId,
'status' => $isDonation ? 'approved' : 'pending',
'status_reason' => $isDonation ? 'Marked as Donation (non-reimbursable).' : null,
'approved_by' => $isDonation ? $userId : null,
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
]);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($expense->toArray()),
], 201);
}
public function update(UpdateExpenseRequest $request, int $id): JsonResponse
{
$expense = Expense::query()->find($id);
if (!$expense) {
return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404);
}
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = $expense->receipt_path;
if ($request->hasFile('receipt')) {
$receiptName = $this->receiptService->storeReceipt($request->file('receipt'));
}
if (!empty($payload['remove_receipt'])) {
$receiptName = null;
}
$category = $payload['category'];
$isDonation = $category === 'Donation';
$updateData = [
'category' => $category,
'amount' => $payload['amount'],
'description' => $payload['description'] ?? null,
'retailor' => $payload['retailor'] ?? null,
'date_of_purchase' => $payload['date_of_purchase'],
'purchased_by' => (int) $payload['purchased_by'],
'receipt_path' => $receiptName,
'updated_by' => $userId,
];
if ($isDonation) {
$updateData['status'] = 'approved';
$updateData['status_reason'] = 'Marked as Donation (non-reimbursable).';
$updateData['approved_by'] = $userId ?: null;
$updateData['reimbursement_id'] = null;
} elseif (($expense->category ?? '') === 'Donation') {
$updateData['status_reason'] = null;
$updateData['approved_by'] = $expense->approved_by;
$updateData['status'] = $expense->status ?? 'pending';
}
$updated = $this->writeService->update($expense, $updateData);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($updated->toArray()),
]);
}
public function updateStatus(UpdateExpenseStatusRequest $request, int $id): JsonResponse
{
$expense = Expense::query()->find($id);
if (!$expense) {
return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404);
}
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$updated = $this->statusService->updateStatus($expense, $payload['status'], $payload['reason'] ?? '', $userId);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($updated->toArray()),
]);
}
}
@@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers\Api\ExtraCharges;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\ExtraCharges\StoreExtraChargeRequest;
use App\Http\Requests\ExtraCharges\UpdateExtraChargeRequest;
use App\Http\Resources\ExtraCharges\ExtraChargeInvoiceOptionResource;
use App\Http\Resources\ExtraCharges\ExtraChargeParentOptionResource;
use App\Http\Resources\ExtraCharges\ExtraChargeResource;
use App\Models\AdditionalCharge;
use App\Services\ExtraCharges\ExtraChargesChargeService;
use App\Services\ExtraCharges\ExtraChargesContextService;
use App\Services\ExtraCharges\ExtraChargesInvoiceService;
use App\Services\ExtraCharges\ExtraChargesListService;
use App\Services\ExtraCharges\ExtraChargesMetaService;
use App\Services\ExtraCharges\ExtraChargesParentService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ExtraChargesController extends BaseApiController
{
public function __construct(
private ExtraChargesContextService $context,
private ExtraChargesMetaService $meta,
private ExtraChargesParentService $parents,
private ExtraChargesInvoiceService $invoices,
private ExtraChargesListService $listService,
private ExtraChargesChargeService $chargeService
) {
parent::__construct();
}
public function list(Request $request): JsonResponse
{
$year = (string) ($request->query('school_year') ?? $this->context->getSchoolYear());
$sem = (string) ($request->query('semester') ?? $this->context->getSemester());
$status = $request->query('status') ?: null;
$q = trim((string) ($request->query('q') ?? '')) ?: null;
$per = (int) ($request->query('per_page') ?? 50);
[$rows, $meta] = $this->listService->listForTerm($year, $sem, $status, $q, $per);
return response()->json([
'ok' => true,
'school_year' => $year,
'semester' => $sem,
'rows' => ExtraChargeResource::collection($rows),
'pager' => $meta,
]);
}
public function options(Request $request): JsonResponse
{
$q = trim((string) ($request->query('q') ?? ''));
$parentOptions = $this->parents->searchParents($q);
return response()->json([
'ok' => true,
'schoolYear' => $this->context->getSchoolYear(),
'semester' => $this->context->getSemester(),
'schoolYears' => $this->meta->getSchoolYears($this->context->getSchoolYear()),
'parents' => ExtraChargeParentOptionResource::collection($parentOptions),
]);
}
public function parentOptions(Request $request): JsonResponse
{
$q = trim((string) ($request->query('q') ?? ''));
$parentOptions = $this->parents->searchParents($q);
return response()->json([
'results' => ExtraChargeParentOptionResource::collection($parentOptions),
]);
}
public function invoicesForParent(Request $request): JsonResponse
{
$parentId = (int) ($request->query('parent_id') ?? 0);
$schoolYear = (string) ($request->query('school_year') ?? $this->context->getSchoolYear());
$rows = $this->invoices->listInvoicesForParent($parentId, $schoolYear);
return response()->json([
'results' => ExtraChargeInvoiceOptionResource::collection($rows),
]);
}
public function store(StoreExtraChargeRequest $request): JsonResponse
{
$payload = $request->validated();
$payload['created_by'] = (int) (auth()->id() ?? 0);
$result = $this->chargeService->createCharge($payload);
return response()->json($result, $result['ok'] ? 201 : 422);
}
public function update(UpdateExtraChargeRequest $request, int $id): JsonResponse
{
$charge = AdditionalCharge::query()->find($id);
if (!$charge) {
return response()->json(['ok' => false, 'error' => 'Charge not found'], 404);
}
$payload = $request->validated();
$payload['updated_by'] = (int) (auth()->id() ?? 0);
$ok = $this->chargeService->updateCharge($charge, $payload);
return response()->json([
'ok' => $ok,
'id' => $id,
], $ok ? 200 : 422);
}
public function void(int $id): JsonResponse
{
$charge = AdditionalCharge::query()->find($id);
if (!$charge) {
return response()->json(['ok' => false, 'error' => 'Charge not found'], 404);
}
$ok = $this->chargeService->voidCharge($charge);
return response()->json([
'ok' => $ok,
], $ok ? 200 : 422);
}
public function reverse(int $id): JsonResponse
{
$charge = AdditionalCharge::query()->find($id);
if (!$charge) {
return response()->json(['ok' => false, 'error' => 'Charge not found'], 404);
}
$ok = $this->chargeService->reverseCharge($charge);
return response()->json([
'ok' => $ok,
], $ok ? 200 : 422);
}
}
@@ -0,0 +1,218 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Finance\FinancialReportRequest;
use App\Http\Requests\Finance\FinancialSummaryRequest;
use App\Http\Requests\Finance\FinancialUnpaidParentsRequest;
use App\Http\Resources\Finance\FinancialReportResource;
use App\Http\Resources\Finance\FinancialSummaryResource;
use App\Http\Resources\Finance\FinancialUnpaidParentResource;
use App\Services\Finance\FinancialPdfReportService;
use App\Services\Finance\FinancialReportService;
use App\Services\Finance\FinancialSchoolYearService;
use App\Services\Finance\FinancialSummaryService;
use App\Services\Finance\FinancialUnpaidParentsService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FinancialController extends BaseApiController
{
public function __construct(
private FinancialReportService $reportService,
private FinancialSummaryService $summaryService,
private FinancialUnpaidParentsService $unpaidParentsService,
private FinancialSchoolYearService $schoolYears,
private FinancialPdfReportService $pdfService
) {
parent::__construct();
}
public function report(FinancialReportRequest $request): JsonResponse
{
$payload = $request->validated();
$report = $this->reportService->getReport(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'report' => new FinancialReportResource($report),
]);
}
public function summary(FinancialSummaryRequest $request): JsonResponse
{
$payload = $request->validated();
$summary = $this->summaryService->getSummary(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$schoolYears = $this->schoolYears->listYears($summary['schoolYear'] ?? null);
return response()->json([
'ok' => true,
'summary' => new FinancialSummaryResource($summary),
'schoolYears' => $schoolYears,
]);
}
public function unpaidParents(FinancialUnpaidParentsRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->unpaidParentsService->getUnpaidParents($payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'schoolYear' => $data['schoolYear'] ?? null,
'schoolYears' => $data['schoolYears'] ?? [],
'results' => FinancialUnpaidParentResource::collection($data['results'] ?? []),
]);
}
public function downloadCsv(FinancialReportRequest $request): StreamedResponse
{
$payload = $request->validated();
$report = $this->reportService->getReport(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$paymentsMap = [];
foreach ($report['payments'] as $payment) {
$invoiceId = (int) ($payment['invoice_id'] ?? 0);
$paymentsMap[$invoiceId] = (float) ($payment['paid_amount'] ?? 0);
}
$refundsMap = [];
foreach ($report['refunds'] as $refund) {
$invoiceId = (int) ($refund['invoice_id'] ?? 0);
$refundsMap[$invoiceId] = (float) ($refund['total_refunded'] ?? 0);
}
$discountsMap = [];
foreach ($report['discounts'] as $discount) {
$invoiceId = (int) ($discount['invoice_id'] ?? 0);
$discountsMap[$invoiceId] = (float) ($discount['discount_amount'] ?? 0);
}
$breakdown = $report['paymentBreakdown'] ?? [];
$filename = 'financial_report_' . date('Ymd_His') . '.csv';
return response()->streamDownload(function () use ($report, $paymentsMap, $refundsMap, $discountsMap, $breakdown) {
$out = fopen('php://output', 'w');
fputcsv($out, ['Invoice #', 'Parent', 'School Year', 'Total', 'Paid', 'Cash', 'Credit', 'Check', 'Balance', 'Refund', 'Discount', 'Status']);
$sumTotal = 0.0;
$sumPaid = 0.0;
$sumCash = 0.0;
$sumCredit = 0.0;
$sumCheck = 0.0;
$sumBalance = 0.0;
$sumRefund = 0.0;
$sumDiscount = 0.0;
foreach ($report['invoices'] as $inv) {
$invoiceId = (int) ($inv['id'] ?? 0);
$paid = (float) ($paymentsMap[$invoiceId] ?? 0);
$bd = $breakdown[$invoiceId] ?? [];
$cash = (float) ($bd['cash'] ?? 0);
$credit = (float) ($bd['credit'] ?? 0);
$check = (float) ($bd['check'] ?? 0);
$refunded = (float) ($refundsMap[$invoiceId] ?? 0);
$discount = (float) ($discountsMap[$invoiceId] ?? 0);
$total = (float) ($inv['total_amount'] ?? 0);
$balance = $total - $paid - $discount - $refunded;
if ($balance < 0) {
$balance = 0.0;
}
$status = $balance === 0.0 ? 'Paid' : 'Unpaid';
fputcsv($out, [
$inv['invoice_number'] ?? '',
$inv['parent_name'] ?? '',
$inv['school_year'] ?? '',
$total,
$paid,
$cash,
$credit,
$check,
$balance,
$refunded,
$discount,
$status,
]);
$sumTotal += $total;
$sumPaid += $paid;
$sumCash += $cash;
$sumCredit += $credit;
$sumCheck += $check;
$sumBalance += $balance;
$sumRefund += $refunded;
$sumDiscount += $discount;
}
fputcsv($out, [
'TOTALS',
'',
'',
$sumTotal,
$sumPaid,
$sumCash,
$sumCredit,
$sumCheck,
$sumBalance,
$sumRefund,
$sumDiscount,
'',
]);
fputcsv($out, []);
fputcsv($out, ['Expenses Summary']);
fputcsv($out, ['Category', 'Total Amount']);
foreach ($report['expenses'] as $exp) {
fputcsv($out, [
$exp['category'] ?? '',
$exp['total_amount'] ?? 0,
]);
}
fputcsv($out, []);
fputcsv($out, ['Reimbursements Summary']);
fputcsv($out, ['Status', 'Total Amount']);
foreach ($report['reimbursements'] as $row) {
fputcsv($out, [
$row['status'] ?? '',
$row['total_amount'] ?? 0,
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv']);
}
public function downloadPdf(FinancialReportRequest $request): StreamedResponse
{
$payload = $request->validated();
$summary = $this->summaryService->getSummary(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$pdfBytes = $this->pdfService->buildPdf($summary);
$schoolYear = $summary['schoolYear'] ?? 'report';
return response()->streamDownload(function () use ($pdfBytes) {
echo $pdfBytes;
}, 'Financial_Report_' . $schoolYear . '.pdf', ['Content-Type' => 'application/pdf']);
}
}
@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Invoices\InvoiceGenerateRequest;
use App\Http\Requests\Invoices\InvoiceManagementRequest;
use App\Http\Requests\Invoices\InvoiceParentRequest;
use App\Http\Requests\Invoices\InvoiceStatusRequest;
use App\Http\Resources\Invoices\InvoiceManagementParentResource;
use App\Http\Resources\Invoices\InvoiceResource;
use App\Models\Invoice;
use App\Services\Invoices\InvoiceGenerationService;
use App\Services\Invoices\InvoiceManagementService;
use App\Services\Invoices\InvoicePaymentService;
use App\Services\Invoices\InvoicePdfService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class InvoiceController extends BaseApiController
{
public function __construct(
private InvoiceManagementService $management,
private InvoiceGenerationService $generation,
private InvoicePaymentService $paymentService,
private InvoicePdfService $pdfService
) {
parent::__construct();
}
public function management(InvoiceManagementRequest $request): JsonResponse
{
$schoolYear = $request->validated()['school_year'] ?? null;
$schoolYear = $schoolYear ?: $request->input('schoolYear');
$schoolYear = $schoolYear ?: $request->input('year');
$data = $this->management->getManagementData($schoolYear);
return response()->json([
'ok' => true,
'schoolYear' => $data['schoolYear'],
'schoolYears' => $data['schoolYears'],
'invoices' => InvoiceManagementParentResource::collection($data['invoices']),
]);
}
public function generate(InvoiceGenerateRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->generation->generateInvoice((int) $payload['parent_id'], $payload['school_year'] ?? null);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to generate invoice.'], 422);
}
return response()->json([
'ok' => true,
'updated' => $result['updated'] ?? false,
'updated_ids' => $result['updated_ids'] ?? [],
'insert_id' => $result['insert_id'] ?? null,
]);
}
public function byParent(InvoiceParentRequest $request, int $parentId): JsonResponse
{
$schoolYear = $request->validated()['school_year'] ?? null;
$invoices = Invoice::getInvoicesByParentId($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($invoices),
]);
}
public function parentPayment(InvoiceParentRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$schoolYear = $request->validated()['school_year'] ?? null;
$data = $this->paymentService->getParentInvoiceSummary($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($data['invoices']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
'currentSchoolYear' => $data['currentSchoolYear'],
'dueDate' => $data['dueDate'],
]);
}
public function updateStatus(InvoiceStatusRequest $request, int $invoiceId): JsonResponse
{
$status = $request->validated()['status'];
$updated = Invoice::updateInvoiceStatus($invoiceId, $status);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Invoice not found.'], 404);
}
return response()->json(['ok' => true, 'status' => $status]);
}
public function unpaid(): JsonResponse
{
$rows = Invoice::getUnpaidInvoices();
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($rows),
]);
}
public function pdf(int $invoiceId): StreamedResponse
{
$pdfBytes = $this->pdfService->buildPdf($invoiceId);
return response()->streamDownload(function () use ($pdfBytes) {
echo $pdfBytes;
}, 'invoice_' . $invoiceId . '.pdf', ['Content-Type' => 'application/pdf']);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentByParentRequest;
use App\Http\Requests\Payments\PaymentCreateRequest;
use App\Http\Requests\Payments\PaymentUpdateBalanceRequest;
use App\Http\Resources\Payments\PaymentResource;
use App\Services\Payments\PaymentBalanceService;
use App\Services\Payments\PaymentLookupService;
use App\Services\Payments\PaymentPlanService;
use Illuminate\Http\JsonResponse;
class PaymentController extends BaseApiController
{
public function __construct(
private PaymentPlanService $planService,
private PaymentLookupService $lookupService,
private PaymentBalanceService $balanceService
) {
parent::__construct();
}
public function store(PaymentCreateRequest $request): JsonResponse
{
$payload = $request->validated();
$payment = $this->planService->createPlan($payload);
return response()->json([
'ok' => true,
'payment' => new PaymentResource($payment),
], 201);
}
public function byParent(PaymentByParentRequest $request, int $parentId): JsonResponse
{
$payload = $request->validated();
$payments = $this->lookupService->getByParent($parentId, $payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'payments' => PaymentResource::collection($payments),
]);
}
public function updateBalance(PaymentUpdateBalanceRequest $request, int $paymentId): JsonResponse
{
$payload = $request->validated();
$updated = $this->balanceService->updateBalance($paymentId, (float) $payload['paid_amount']);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Payment not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentEventChargesListRequest;
use App\Http\Requests\Payments\PaymentEventChargesStoreRequest;
use App\Services\Payments\PaymentEventChargesService;
use Illuminate\Http\JsonResponse;
class PaymentEventChargesController extends BaseApiController
{
public function __construct(private PaymentEventChargesService $service)
{
parent::__construct();
}
public function index(PaymentEventChargesListRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->listCharges($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json(['ok' => true] + $data);
}
public function store(PaymentEventChargesStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$count = $this->service->addCharges($payload, $userId);
if ($count === 0) {
return response()->json(['ok' => false, 'message' => 'No student selected.'], 422);
}
return response()->json(['ok' => true, 'inserted' => $count]);
}
public function enrolledStudents(PaymentEventChargesListRequest $request, int $parentId): JsonResponse
{
$payload = $request->validated();
$students = $this->service->getEnrolledStudents($parentId, $payload['school_year'] ?? null);
return response()->json(['ok' => true, 'students' => $students]);
}
}
@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentManualEditRequest;
use App\Http\Requests\Payments\PaymentManualSearchRequest;
use App\Http\Requests\Payments\PaymentManualSuggestRequest;
use App\Http\Requests\Payments\PaymentManualUpdateRequest;
use App\Services\Payments\PaymentManualService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PaymentManualController extends BaseApiController
{
public function __construct(private PaymentManualService $service)
{
parent::__construct();
}
public function search(PaymentManualSearchRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->search(
(string) ($payload['search_term'] ?? ''),
$payload['school_year'] ?? null
);
return response()->json(['ok' => true] + $data);
}
public function suggest(PaymentManualSuggestRequest $request): JsonResponse
{
$payload = $request->validated();
$items = $this->service->suggest((string) ($payload['q'] ?? ''));
return response()->json(['ok' => true, 'items' => $items]);
}
public function record(PaymentManualUpdateRequest $request, int $invoiceId): JsonResponse
{
$payload = $request->validated();
$result = $this->service->recordPayment(
$invoiceId,
(float) $payload['amount'],
(string) $payload['payment_method'],
$payload['payment_date'] ?? null,
$payload['check_number'] ?? null,
$payload['payment_type'] ?? null,
$request->file('payment_file'),
$payload['school_year'] ?? null,
$payload['semester'] ?? null,
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true] + $result);
}
public function edit(PaymentManualEditRequest $request, int $paymentId): JsonResponse
{
$payload = $request->validated();
$this->service->editPayment(
$paymentId,
(float) $payload['paid_amount'],
(string) $payload['payment_method'],
$payload['check_number'] ?? null,
$request->file('payment_file'),
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function file(Request $request, string $filename)
{
$mode = (string) ($request->query('mode') ?? 'download');
return $this->service->serveCheckFile($filename, $mode);
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentNotificationListRequest;
use App\Http\Requests\Payments\PaymentNotificationSendRequest;
use App\Http\Resources\Payments\PaymentNotificationLogResource;
use App\Services\Payments\PaymentNotificationService;
use Illuminate\Http\JsonResponse;
class PaymentNotificationController extends BaseApiController
{
public function __construct(private PaymentNotificationService $service)
{
parent::__construct();
}
public function index(PaymentNotificationListRequest $request): JsonResponse
{
$payload = $request->validated();
$logs = $this->service->listLogs(
$payload['from'] ?? null,
$payload['to'] ?? null,
$payload['type'] ?? null
);
return response()->json([
'ok' => true,
'logs' => PaymentNotificationLogResource::collection($logs),
]);
}
public function send(PaymentNotificationSendRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->send($payload);
return response()->json(['ok' => true] + $result);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentTransactionCreateRequest;
use App\Http\Requests\Payments\PaymentTransactionStatusRequest;
use App\Http\Resources\Payments\PaymentTransactionResource;
use App\Services\Payments\PaymentTransactionService;
use Illuminate\Http\JsonResponse;
class PaymentTransactionController extends BaseApiController
{
public function __construct(private PaymentTransactionService $service)
{
parent::__construct();
}
public function store(PaymentTransactionCreateRequest $request): JsonResponse
{
$payload = $request->validated();
$transaction = $this->service->create($payload);
return response()->json([
'ok' => true,
'transaction' => new PaymentTransactionResource($transaction->toArray()),
], 201);
}
public function byPayment(int $paymentId): JsonResponse
{
$transactions = $this->service->getByPayment($paymentId);
return response()->json([
'ok' => true,
'transactions' => PaymentTransactionResource::collection($transactions),
]);
}
public function updateStatus(PaymentTransactionStatusRequest $request, string $transactionId): JsonResponse
{
$payload = $request->validated();
$updated = $this->service->updateStatus($transactionId, $payload['status']);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Transaction not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaypalExecuteRequest;
use App\Services\Payments\PaypalPaymentService;
use Illuminate\Http\JsonResponse;
class PaypalPaymentController extends BaseApiController
{
public function __construct(private PaypalPaymentService $service)
{
parent::__construct();
}
public function redirect(): JsonResponse
{
return response()->json([
'ok' => true,
'redirect_url' => $this->service->getRedirectUrl(),
]);
}
public function create(int $paymentId): JsonResponse
{
$result = $this->service->createPayment($paymentId);
return response()->json(['ok' => true] + $result);
}
public function execute(PaypalExecuteRequest $request): JsonResponse
{
$payload = $request->validated();
$this->service->executePayment(
(string) $payload['payer_id'],
$payload['paypal_payment_id'] ?? null
);
return response()->json(['ok' => true]);
}
public function cancel(): JsonResponse
{
$this->service->cancelPayment();
return response()->json([
'ok' => true,
'message' => 'Payment was canceled.',
]);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaypalTransactionsListRequest;
use App\Http\Resources\Payments\PaypalTransactionResource;
use App\Services\Payments\PaypalTransactionsService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PaypalTransactionsController extends BaseApiController
{
public function __construct(private PaypalTransactionsService $service)
{
parent::__construct();
}
public function index(PaypalTransactionsListRequest $request): JsonResponse
{
$payload = $request->validated();
$perPage = (int) ($payload['per_page'] ?? 10);
$rows = $this->service->list($payload['q'] ?? null, $perPage);
return response()->json([
'ok' => true,
'transactions' => PaypalTransactionResource::collection($rows->items()),
'pagination' => [
'current_page' => $rows->currentPage(),
'per_page' => $rows->perPage(),
'total' => $rows->total(),
'last_page' => $rows->lastPage(),
],
]);
}
public function downloadCsv(PaypalTransactionsListRequest $request): StreamedResponse
{
$payload = $request->validated();
$rows = $this->service->listAll($payload['q'] ?? null);
$filename = 'paypal_transactions_' . date('Ymd_His') . '.csv';
return response()->streamDownload(function () use ($rows) {
$out = fopen('php://output', 'w');
fputcsv($out, [
'ID', 'Transaction ID', 'Order ID', 'Parent School ID',
'Email', 'Amount', 'Net Amount', 'Currency',
'Status', 'Event Type', 'Created At',
]);
foreach ($rows as $row) {
fputcsv($out, [
$row['id'] ?? null,
$row['transaction_id'] ?? null,
$row['order_id'] ?? null,
$row['parent_school_id'] ?? null,
$row['payer_email'] ?? null,
$row['amount'] ?? null,
$row['net_amount'] ?? null,
$row['currency'] ?? null,
$row['status'] ?? null,
$row['event_type'] ?? null,
$row['created_at'] ?? null,
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv']);
}
}
@@ -0,0 +1,391 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Files\FileNameRequest;
use App\Http\Requests\Reimbursements\ReimbursementBatchAdminFileRequest;
use App\Http\Requests\Reimbursements\ReimbursementBatchAssignmentRequest;
use App\Http\Requests\Reimbursements\ReimbursementBatchCreateRequest;
use App\Http\Requests\Reimbursements\ReimbursementBatchEmailRequest;
use App\Http\Requests\Reimbursements\ReimbursementBatchLockRequest;
use App\Http\Requests\Reimbursements\ReimbursementExportBatchRequest;
use App\Http\Requests\Reimbursements\ReimbursementExportRequest;
use App\Http\Requests\Reimbursements\ReimbursementIndexRequest;
use App\Http\Requests\Reimbursements\ReimbursementMarkDonationRequest;
use App\Http\Requests\Reimbursements\ReimbursementProcessRequest;
use App\Http\Requests\Reimbursements\ReimbursementStoreRequest;
use App\Http\Requests\Reimbursements\ReimbursementUnderProcessingRequest;
use App\Http\Requests\Reimbursements\ReimbursementUpdateRequest;
use App\Http\Resources\Reimbursements\ReimbursementBatchResource;
use App\Http\Resources\Reimbursements\ReimbursementExpenseResource;
use App\Http\Resources\Reimbursements\ReimbursementRecipientResource;
use App\Http\Resources\Reimbursements\ReimbursementResource;
use App\Http\Resources\Reimbursements\ReimbursementUnderProcessingItemResource;
use App\Models\Reimbursement;
use App\Models\ReimbursementBatch;
use App\Services\Files\FileServeService;
use App\Services\Reimbursements\ReimbursementBatchAssignmentService;
use App\Services\Reimbursements\ReimbursementBatchService;
use App\Services\Reimbursements\ReimbursementContextService;
use App\Services\Reimbursements\ReimbursementCrudService;
use App\Services\Reimbursements\ReimbursementDonationService;
use App\Services\Reimbursements\ReimbursementEmailService;
use App\Services\Reimbursements\ReimbursementExportService;
use App\Services\Reimbursements\ReimbursementFileService;
use App\Services\Reimbursements\ReimbursementQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use RuntimeException;
class ReimbursementController extends BaseApiController
{
public function __construct(
private ReimbursementContextService $context,
private ReimbursementQueryService $queryService,
private ReimbursementDonationService $donationService,
private ReimbursementBatchService $batchService,
private ReimbursementBatchAssignmentService $assignmentService,
private ReimbursementFileService $fileService,
private ReimbursementExportService $exportService,
private ReimbursementEmailService $emailService,
private ReimbursementCrudService $crudService,
private FileServeService $fileServeService
) {
parent::__construct();
}
public function underProcessing(ReimbursementUnderProcessingRequest $request): JsonResponse
{
$data = $this->queryService->underProcessing();
return response()->json([
'ok' => true,
'pendingItems' => ReimbursementUnderProcessingItemResource::collection($data['pendingItems'] ?? []),
'existingBatches' => ReimbursementBatchResource::collection($data['existingBatches'] ?? []),
'adminUsers' => ReimbursementRecipientResource::collection($data['adminUsers'] ?? []),
'itemsPayload' => ReimbursementUnderProcessingItemResource::collection($data['itemsPayload'] ?? []),
]);
}
public function markDonation(ReimbursementMarkDonationRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
try {
$this->donationService->markDonation((int) $payload['expense_id'], $userId);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$status = str_contains($message, 'not found') ? 404 : (str_contains($message, 'already') ? 409 : 422);
return response()->json(['ok' => false, 'message' => $message], $status);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to mark donation right now.'], 500);
}
return response()->json(['ok' => true]);
}
public function createBatch(ReimbursementBatchCreateRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$schoolYear = $this->context->getSchoolYear();
$semester = $this->context->getSemester();
$batch = $this->batchService->createBatch($payload['title'] ?? null, $userId, $schoolYear, $semester);
return response()->json([
'ok' => true,
'batch' => $batch,
], 201);
}
public function updateBatchAssignment(ReimbursementBatchAssignmentRequest $request): JsonResponse
{
$payload = $request->validated();
$batchId = (int) ($payload['batch_id'] ?? 0);
$batchNumber = $payload['batch_number'] ?? null;
if ($batchId <= 0 && $batchNumber !== null && trim((string) $batchNumber) !== '') {
$batchId = (int) $batchNumber;
}
$adminIdRaw = $payload['admin_id'] ?? null;
$adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;
$reimbursementId = !empty($payload['reimbursement_id']) ? (int) $payload['reimbursement_id'] : null;
try {
$result = $this->assignmentService->updateAssignment(
(int) $payload['expense_id'],
$batchId,
$adminId,
$reimbursementId,
$this->context->getSchoolYear(),
$this->context->getSemester()
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json([
'ok' => true,
'assignment' => $result,
]);
}
public function lockBatch(ReimbursementBatchLockRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
try {
$this->batchService->lockBatch(
(int) $payload['batch_id'],
$userId,
$this->context->getSchoolYear(),
$this->context->getSemester()
);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$status = str_contains($message, 'check file') ? 409 : 404;
return response()->json(['ok' => false, 'message' => $message], $status);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to lock batch right now.'], 500);
}
return response()->json(['ok' => true, 'status' => 'closed']);
}
public function uploadBatchAdminFile(ReimbursementBatchAdminFileRequest $request): JsonResponse
{
$payload = $request->validated();
$batchId = (int) $payload['batch_id'];
$adminId = (int) $payload['admin_id'];
$batch = ReimbursementBatch::query()->find($batchId);
if (!$batch) {
return response()->json(['ok' => false, 'message' => 'Batch not found.'], 404);
}
if (strtolower((string) ($batch->status ?? '')) !== ReimbursementBatch::STATUS_OPEN) {
return response()->json(['ok' => false, 'message' => 'Cannot upload files for a locked batch.'], 409);
}
try {
$file = $this->fileService->storeAdminFile($batchId, $adminId, $request->file('check_file'), (int) (auth()->id() ?? 0));
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to store the uploaded file.'], 500);
}
return response()->json([
'ok' => true,
'file' => $file,
]);
}
public function serveAdminCheckFile(FileNameRequest $request, string $name, string $mode = 'inline'): Response|JsonResponse
{
$allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'];
$meta = $this->fileServeService->meta(storage_path('uploads/reimbursements'), $name, $allowedExtensions, null);
$disposition = strtolower(trim($mode)) === 'download' ? 'attachment' : 'inline';
return response(file_get_contents($meta['path']), 200, [
'Content-Type' => $meta['mime'],
'Content-Disposition' => $disposition . '; filename="' . $meta['download_name'] . '"',
'Content-Length' => (string) $meta['size'],
'ETag' => $meta['etag'],
'Last-Modified' => $meta['last_modified'],
]);
}
public function index(ReimbursementIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$filters = [
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
'status' => $payload['status'] ?? null,
'user_id' => $payload['user_id'] ?? null,
];
$data = $this->queryService->index($filters);
return response()->json([
'ok' => true,
'expenses' => ReimbursementExpenseResource::collection($data['expenses'] ?? []),
'users' => ReimbursementRecipientResource::collection($data['users'] ?? []),
'schoolYears' => $data['schoolYears'] ?? [],
'batchSummaries' => $data['batchSummaries'] ?? [],
'batchDetails' => $data['batchDetails'] ?? [],
'batchAttachments' => $data['batchAttachments'] ?? [],
'donationBatch' => $data['donationBatch'] ?? null,
'donationDetails' => $data['donationDetails'] ?? null,
]);
}
public function sendBatchEmail(ReimbursementBatchEmailRequest $request): JsonResponse
{
$payload = $request->validated();
$receiptIds = $payload['receipts'] ?? [];
$checkIds = $payload['checks'] ?? [];
try {
$ok = $this->emailService->sendBatchEmail(
(int) $payload['batch_id'],
$payload['recipient_email'],
(string) ($payload['message'] ?? ''),
$receiptIds,
$checkIds
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 404);
}
if (!$ok) {
return response()->json(['ok' => false, 'message' => 'Failed to send email. Please try again.'], 500);
}
return response()->json(['ok' => true, 'message' => 'Email sent successfully.']);
}
public function export(ReimbursementExportRequest $request): Response
{
$payload = $request->validated();
$type = $payload['type'] ?? 'processed';
if ($type === 'under_processing') {
$csv = $this->exportService->buildUnderProcessingCsv();
} else {
$csv = $this->exportService->buildProcessedCsv($payload);
}
return response()->streamDownload(function () use ($csv) {
$out = fopen('php://output', 'w');
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($csv['rows'] as $row) {
fputcsv($out, $row);
}
fclose($out);
}, $csv['filename'], [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
public function exportBatch(ReimbursementExportBatchRequest $request): Response
{
$payload = $request->validated();
$csv = $this->exportService->buildBatchCsv((int) $payload['batch_id']);
return response()->streamDownload(function () use ($csv) {
$out = fopen('php://output', 'w');
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($csv['rows'] as $row) {
fputcsv($out, $row);
}
fclose($out);
}, $csv['filename'], [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
public function store(ReimbursementStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = null;
if ($request->hasFile('receipt')) {
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
}
try {
$reimb = $this->crudService->store(
$payload,
$userId,
$this->context->getSchoolYear(),
$this->context->getSemester(),
$receiptName
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
$data = $reimb->toArray();
$data['receipt_url'] = $this->fileService->receiptUrl($reimb->receipt_path ?? null);
return response()->json([
'ok' => true,
'reimbursement' => new ReimbursementResource($data),
], 201);
}
public function process(ReimbursementProcessRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = null;
if ($request->hasFile('receipt')) {
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
}
try {
$reimb = $this->crudService->process(
$payload,
$userId,
$this->context->getSchoolYear(),
$this->context->getSemester(),
$receiptName
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
$data = $reimb->toArray();
$data['receipt_url'] = $this->fileService->receiptUrl($reimb->receipt_path ?? null);
return response()->json([
'ok' => true,
'reimbursement' => new ReimbursementResource($data),
], 201);
}
public function reimbursedExpenses(): JsonResponse
{
$expenses = $this->queryService->reimbursedExpenses();
return response()->json([
'ok' => true,
'expenses' => $expenses,
]);
}
public function update(ReimbursementUpdateRequest $request, int $id): JsonResponse
{
$reimb = Reimbursement::query()->find($id);
if (!$reimb) {
return response()->json(['ok' => false, 'message' => 'Reimbursement not found.'], 404);
}
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = $reimb->receipt_path;
if ($request->hasFile('receipt')) {
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
}
if (!empty($payload['remove_receipt'])) {
$receiptName = null;
}
$updated = $this->crudService->update($reimb, $payload, $userId, $receiptName);
$data = $updated->toArray();
$data['receipt_url'] = $this->fileService->receiptUrl($updated->receipt_path ?? null);
return response()->json([
'ok' => true,
'reimbursement' => new ReimbursementResource($data),
]);
}
}
@@ -0,0 +1,281 @@
<?php
namespace App\Http\Controllers\Api\Grading;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Grading\BelowSixtyEmailRequest;
use App\Http\Requests\Grading\BelowSixtyMeetingRequest;
use App\Http\Requests\Grading\BelowSixtyMeetingSaveRequest;
use App\Http\Requests\Grading\BelowSixtySendRequest;
use App\Http\Requests\Grading\BelowSixtyStatusRequest;
use App\Http\Requests\Grading\BelowSixtyListRequest;
use App\Http\Requests\Grading\GradingLockAllRequest;
use App\Http\Requests\Grading\GradingLockRequest;
use App\Http\Requests\Grading\GradingOverviewRequest;
use App\Http\Requests\Grading\GradingRefreshRequest;
use App\Http\Requests\Grading\GradingScoreShowRequest;
use App\Http\Requests\Grading\GradingScoreUpdateRequest;
use App\Http\Requests\Grading\PlacementBatchRequest;
use App\Http\Requests\Grading\PlacementBatchUpdateRequest;
use App\Http\Requests\Grading\PlacementLevelsRequest;
use App\Http\Requests\Grading\PlacementLevelRequest;
use App\Http\Requests\Grading\PlacementRequest;
use App\Http\Resources\Grading\BelowSixtyStudentResource;
use App\Http\Resources\Grading\GradingScoreResource;
use App\Services\Grading\GradingBelowSixtyService;
use App\Services\Grading\GradingLockService;
use App\Services\Grading\GradingOverviewService;
use App\Services\Grading\GradingPlacementService;
use App\Services\Grading\GradingRefreshService;
use App\Services\Grading\GradingScoreService;
use Illuminate\Http\JsonResponse;
class GradingController extends BaseApiController
{
public function __construct(
private GradingOverviewService $overviewService,
private GradingScoreService $scoreService,
private GradingLockService $lockService,
private GradingRefreshService $refreshService,
private GradingPlacementService $placementService,
private GradingBelowSixtyService $belowSixtyService
) {
parent::__construct();
}
public function overview(GradingOverviewRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->overviewService->overview(
isset($payload['class_id']) ? (int) $payload['class_id'] : null,
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
return response()->json(['ok' => true] + $data);
}
public function show(GradingScoreShowRequest $request, string $type, int $classSectionId, int $studentId): JsonResponse
{
$payload = $request->validated();
$data = $this->scoreService->show(
$type,
$classSectionId,
$studentId,
(string) $payload['semester'],
(string) $payload['school_year']
);
return response()->json([
'ok' => true,
'student' => $data['student'],
'scores' => GradingScoreResource::collection($data['scores']),
'type' => $data['type'],
'class_section_id' => $data['class_section_id'],
'semester' => $data['semester'],
'scores_locked' => $data['scores_locked'],
]);
}
public function update(GradingScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$this->scoreService->update($payload, (int) (auth()->id() ?? 0));
return response()->json(['ok' => true]);
}
public function toggleLock(GradingLockRequest $request): JsonResponse
{
$payload = $request->validated();
$locked = $this->lockService->toggle(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'locked' => $locked]);
}
public function lockAll(GradingLockAllRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->lockService->lockAll(
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'locked_count' => $count]);
}
public function refresh(GradingRefreshRequest $request): JsonResponse
{
$payload = $request->validated();
$this->refreshService->refreshSemesterScores(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year']
);
return response()->json(['ok' => true]);
}
public function placement(PlacementRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->placementService->placementContext(
isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null,
(string) $payload['school_year'],
$payload['placement_test'] ?? null,
$payload['open'] ?? null
);
return response()->json(['ok' => true] + $data);
}
public function updatePlacementLevel(PlacementLevelRequest $request): JsonResponse
{
$payload = $request->validated();
$this->placementService->updatePlacementLevel(
(int) $payload['student_id'],
(string) $payload['school_year'],
$payload['placement_level'] ?? null,
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function updatePlacementLevels(PlacementLevelsRequest $request): JsonResponse
{
$payload = $request->validated();
$this->placementService->updatePlacementLevels(
(int) $payload['class_section_id'],
(string) $payload['school_year'],
$payload['placement_level'] ?? [],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function createPlacementBatch(PlacementBatchRequest $request): JsonResponse
{
$payload = $request->validated();
$batchId = $this->placementService->createPlacementBatch(
(string) $payload['school_year'],
(string) $payload['placement_test'],
$payload['placement_level'] ?? [],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'batch_id' => $batchId]);
}
public function showPlacementBatch(int $batchId): JsonResponse
{
$data = $this->placementService->getPlacementBatch($batchId);
return response()->json(['ok' => true] + $data);
}
public function updatePlacementBatch(PlacementBatchUpdateRequest $request, int $batchId): JsonResponse
{
$payload = $request->validated();
$this->placementService->updatePlacementBatch(
$batchId,
(string) $payload['school_year'],
$payload['placement_level'] ?? [],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function belowSixty(BelowSixtyListRequest $request): JsonResponse
{
$payload = $request->validated();
$rows = $this->belowSixtyService->listRows(
(string) $payload['school_year'],
(string) $payload['semester']
);
return response()->json([
'ok' => true,
'rows' => BelowSixtyStudentResource::collection($rows),
'semester' => $payload['semester'],
'school_year' => $payload['school_year'],
]);
}
public function belowSixtyEmail(BelowSixtyEmailRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->belowSixtyService->emailContext(
(int) $payload['student_id'],
(string) $payload['school_year'],
(string) $payload['semester']
);
return response()->json(['ok' => true] + $data);
}
public function sendBelowSixtyEmail(BelowSixtySendRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->belowSixtyService->emailContext(
(int) $payload['student_id'],
(string) $payload['school_year'],
(string) $payload['semester']
);
$subject = trim((string) ($payload['subject'] ?? ''));
if ($subject !== '') {
$data['subject'] = $subject;
}
if (isset($payload['html']) && trim((string) $payload['html']) !== '') {
$data['html'] = (string) $payload['html'];
}
$this->belowSixtyService->sendEmail($data);
return response()->json(['ok' => true]);
}
public function updateBelowSixtyStatus(BelowSixtyStatusRequest $request): JsonResponse
{
$payload = $request->validated();
$this->belowSixtyService->updateStatus(
(int) $payload['student_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(string) $payload['status'],
(string) ($payload['note'] ?? ''),
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function belowSixtyMeeting(BelowSixtyMeetingRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->belowSixtyService->meetingContext(
(int) $payload['student_id'],
(string) $payload['school_year'],
(string) $payload['semester']
);
return response()->json(['ok' => true] + $data);
}
public function saveBelowSixtyMeeting(BelowSixtyMeetingSaveRequest $request): JsonResponse
{
$payload = $request->validated();
$this->belowSixtyService->saveMeeting($payload, (int) (auth()->id() ?? 0));
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Api\Grading;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Grading\HomeworkTrackingRequest;
use App\Http\Resources\Grading\HomeworkTrackingTeacherResource;
use App\Services\Grading\HomeworkTrackingService;
use Illuminate\Http\JsonResponse;
class HomeworkTrackingController extends BaseApiController
{
public function __construct(private HomeworkTrackingService $service)
{
parent::__construct();
}
public function index(HomeworkTrackingRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->report(
$payload['semester'] ?? null,
$payload['school_year'] ?? null,
isset($payload['page']) ? (int) $payload['page'] : 1
);
return response()->json([
'ok' => true,
'semester' => $data['semester'],
'school_year' => $data['school_year'],
'sundays' => $data['sundays'],
'event_days' => $data['event_days'],
'date_to_index' => $data['date_to_index'],
'teachers' => HomeworkTrackingTeacherResource::collection($data['teachers']),
'has_homework' => $data['has_homework'],
'hw_entered_at' => $data['hw_entered_at'],
'has_homework_by_date' => $data['has_homework_by_date'],
'hw_entered_at_by_date' => $data['hw_entered_at_by_date'],
'page' => $data['page'],
'total_pages' => $data['total_pages'],
'per_page' => $data['per_page'],
'total_rows' => $data['total_rows'],
]);
}
}
@@ -0,0 +1,151 @@
<?php
namespace App\Http\Controllers\Api\Incidents;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Incidents\IncidentCancelRequest;
use App\Http\Requests\Incidents\IncidentCloseRequest;
use App\Http\Requests\Incidents\IncidentListRequest;
use App\Http\Requests\Incidents\IncidentStateRequest;
use App\Http\Requests\Incidents\IncidentStoreRequest;
use App\Http\Resources\Incidents\IncidentAnalysisStudentResource;
use App\Http\Resources\Incidents\IncidentGradeResource;
use App\Http\Resources\Incidents\IncidentResource;
use App\Http\Resources\Incidents\IncidentStudentOptionResource;
use App\Services\Incidents\CurrentIncidentService;
use App\Services\Incidents\IncidentAnalysisService;
use App\Services\Incidents\IncidentHistoryService;
use Illuminate\Http\JsonResponse;
class IncidentController extends BaseApiController
{
public function __construct(
private CurrentIncidentService $currentService,
private IncidentHistoryService $historyService,
private IncidentAnalysisService $analysisService
) {
parent::__construct();
}
public function current(): JsonResponse
{
$data = $this->currentService->listCurrent();
return response()->json([
'ok' => true,
'incidents' => IncidentResource::collection($data['incidents']),
'grades' => IncidentGradeResource::collection($data['grades']),
]);
}
public function history(IncidentListRequest $request): JsonResponse
{
$payload = $request->validated();
$incidents = $this->historyService->history($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'incidents' => IncidentResource::collection($incidents),
]);
}
public function processed(IncidentListRequest $request): JsonResponse
{
$payload = $request->validated();
$incidents = $this->historyService->processed($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'incidents' => IncidentResource::collection($incidents),
]);
}
public function analysis(IncidentListRequest $request): JsonResponse
{
$payload = $request->validated();
$students = $this->analysisService->analyze($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'students' => IncidentAnalysisStudentResource::collection($students),
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
]);
}
public function studentsByGrade(int $gradeId): JsonResponse
{
$students = $this->currentService->studentsByGrade($gradeId);
return response()->json([
'ok' => true,
'students' => IncidentStudentOptionResource::collection($students),
]);
}
public function store(IncidentStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->currentService->addIncident($payload, (int) (auth()->id() ?? 0));
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to add incident.'], 422);
}
return response()->json([
'ok' => true,
'created' => $result['created'],
'incident_id' => $result['incident_id'],
]);
}
public function updateState(IncidentStateRequest $request, int $incidentId): JsonResponse
{
$payload = $request->validated();
$updated = $this->currentService->updateState($incidentId, (string) $payload['incident_state']);
return response()->json([
'ok' => $updated,
]);
}
public function close(IncidentCloseRequest $request, int $incidentId): JsonResponse
{
$payload = $request->validated();
$result = $this->currentService->closeIncident(
$incidentId,
(string) $payload['state_description'],
$payload['action_taken'] ?? null,
(int) (auth()->id() ?? 0)
);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to close incident.'], 422);
}
return response()->json([
'ok' => true,
'incident_id' => $result['incident_id'],
]);
}
public function cancel(IncidentCancelRequest $request, int $incidentId): JsonResponse
{
$payload = $request->validated();
$result = $this->currentService->cancelIncident(
$incidentId,
$payload['state_description'] ?? null,
$payload['action_taken'] ?? null,
(int) (auth()->id() ?? 0)
);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to cancel incident.'], 422);
}
return response()->json([
'ok' => true,
'incident_id' => $result['incident_id'],
]);
}
}
@@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Files\FileNameRequest;
use App\Http\Resources\Files\FileMetaResource;
use App\Services\Files\ExamDraftDownloadNameService;
use App\Services\Files\FileServeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class FilesController extends BaseApiController
{
public function __construct(
private FileServeService $fileService,
private ExamDraftDownloadNameService $draftNameService
) {
parent::__construct();
}
public function receipt(FileNameRequest $request, string $name): Response|JsonResponse
{
return $this->serveFile(
$request,
storage_path('uploads/receipts'),
$name,
['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'],
null,
false
);
}
public function reimb(FileNameRequest $request, string $name): Response|JsonResponse
{
return $this->serveFile(
$request,
storage_path('uploads/reimbursements'),
$name,
['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'],
null,
true
);
}
public function earlyDismissalSignature(FileNameRequest $request, string $name): Response|JsonResponse
{
return $this->serveFile(
$request,
storage_path('uploads/early_dismissal_signatures'),
$name,
['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'],
null,
true
);
}
public function examDraftTeacher(FileNameRequest $request, string $name): Response|JsonResponse
{
$downloadName = $this->draftNameService->build($name, 'drafts');
return $this->serveFile(
$request,
storage_path('uploads/exams/drafts'),
$name,
['doc', 'docx', 'pdf'],
$downloadName,
true
);
}
public function examDraftFinal(FileNameRequest $request, string $name): Response|JsonResponse
{
$downloadName = $this->draftNameService->build($name, 'finals');
return $this->serveFile(
$request,
storage_path('uploads/exams/finals'),
$name,
['doc', 'docx', 'pdf'],
$downloadName,
true
);
}
private function serveFile(
FileNameRequest $request,
string $baseDir,
string $name,
array $allowedExtensions,
?string $downloadName,
bool $nosniff
): Response|JsonResponse {
if ($request->boolean('meta')) {
$meta = $this->fileService->meta($baseDir, $name, $allowedExtensions, $downloadName);
return response()->json([
'ok' => true,
'file' => new FileMetaResource($meta),
]);
}
return $this->fileService->serveInline($baseDir, $name, $allowedExtensions, $request, $downloadName, $nosniff);
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreClassRequest;
use App\Http\Requests\Scores\ScoreUpdateRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Models\Student;
use App\Services\Scores\ExamScoreService;
use App\Services\Scores\SemesterScoreService;
use Illuminate\Http\JsonResponse;
class FinalController extends BaseApiController
{
public function __construct(
private ExamScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(ScoreClassRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->list('final_exam', (int) $payload['class_section_id'], $payload['semester'] ?? null, $payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
'final_exam',
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreAddColumnRequest;
use App\Http\Requests\Scores\ScoreClassRequest;
use App\Http\Requests\Scores\ScoreUpdateRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Models\Student;
use App\Services\Scores\HomeworkScoreService;
use App\Services\Scores\SemesterScoreService;
use Illuminate\Http\JsonResponse;
class HomeworkController extends BaseApiController
{
public function __construct(
private HomeworkScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(ScoreClassRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->list(
(int) $payload['class_section_id'],
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'headers' => $data['headers'],
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function addColumn(ScoreAddColumnRequest $request): JsonResponse
{
$payload = $request->validated();
$nextIndex = $this->service->addColumn(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'homework_index' => $nextIndex]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreClassRequest;
use App\Http\Requests\Scores\ScoreUpdateRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Models\Student;
use App\Services\Scores\ExamScoreService;
use App\Services\Scores\SemesterScoreService;
use Illuminate\Http\JsonResponse;
class MidtermController extends BaseApiController
{
public function __construct(
private ExamScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(ScoreClassRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->list('midterm_exam', (int) $payload['class_section_id'], $payload['semester'] ?? null, $payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
'midterm_exam',
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreClassRequest;
use App\Http\Requests\Scores\ScoreUpdateRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Models\Student;
use App\Services\Scores\ParticipationScoreService;
use App\Services\Scores\SemesterScoreService;
use Illuminate\Http\JsonResponse;
class ParticipationController extends BaseApiController
{
public function __construct(
private ParticipationScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(ScoreClassRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->list(
(int) $payload['class_section_id'],
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreAddColumnRequest;
use App\Http\Requests\Scores\ScoreClassRequest;
use App\Http\Requests\Scores\ScoreUpdateRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Models\Student;
use App\Services\Scores\ProjectScoreService;
use App\Services\Scores\SemesterScoreService;
use Illuminate\Http\JsonResponse;
class ProjectController extends BaseApiController
{
public function __construct(
private ProjectScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(ScoreClassRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->list(
(int) $payload['class_section_id'],
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'headers' => $data['headers'],
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function addColumn(ScoreAddColumnRequest $request): JsonResponse
{
$payload = $request->validated();
$nextIndex = $this->service->addColumn(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'project_index' => $nextIndex]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
// swallow errors to keep API response stable
}
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreAddColumnRequest;
use App\Http\Requests\Scores\ScoreClassRequest;
use App\Http\Requests\Scores\ScoreUpdateRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Models\Student;
use App\Services\Scores\QuizScoreService;
use App\Services\Scores\SemesterScoreService;
use Illuminate\Http\JsonResponse;
class QuizController extends BaseApiController
{
public function __construct(
private QuizScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(ScoreClassRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->list(
(int) $payload['class_section_id'],
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'headers' => $data['headers'],
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function addColumn(ScoreAddColumnRequest $request): JsonResponse
{
$payload = $request->validated();
$nextIndex = $this->service->addColumn(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'quiz_index' => $nextIndex]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreCommentListRequest;
use App\Http\Requests\Scores\ScoreCommentSaveRequest;
use App\Http\Requests\Scores\ScoreCommentUpdateRequest;
use App\Http\Resources\Scores\ScoreCommentResource;
use App\Services\Scores\ScoreCommentService;
use Illuminate\Http\JsonResponse;
class ScoreCommentController extends BaseApiController
{
public function __construct(private ScoreCommentService $service)
{
parent::__construct();
}
public function index(ScoreCommentListRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->list(
$payload['class_section_id'] ?? null,
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
$studentRows = [];
foreach ($data['students'] as $student) {
$studentId = (int) ($student['student_id'] ?? 0);
$studentRows[] = [
'student_id' => $studentId,
'firstname' => $student['firstname'] ?? null,
'lastname' => $student['lastname'] ?? null,
'school_id' => $student['school_id'] ?? null,
'comments' => $data['comments'][$studentId] ?? [],
];
}
return response()->json([
'ok' => true,
'students' => ScoreCommentResource::collection($studentRows),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'class_section_id' => $data['classSectionId'],
'scores_locked' => $data['scoresLocked'],
]);
}
public function store(ScoreCommentSaveRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->save(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['comments'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
if (!empty($result['errors'])) {
return response()->json(['ok' => false, 'errors' => $result['errors']], 422);
}
return response()->json(['ok' => true]);
}
public function update(ScoreCommentUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['comments'] ?? [],
$payload['reviews'] ?? [],
(int) (auth()->id() ?? 0)
);
if (!empty($result['errors'])) {
return response()->json(['ok' => false, 'errors' => $result['errors']], 422);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreLockRequest;
use App\Http\Requests\Scores\ScoreOverviewRequest;
use App\Http\Requests\Scores\ScoreStudentRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Services\Scores\ScoreDashboardService;
use Illuminate\Http\JsonResponse;
class ScoreController extends BaseApiController
{
public function __construct(private ScoreDashboardService $service)
{
parent::__construct();
}
public function overview(ScoreOverviewRequest $request): JsonResponse
{
$payload = $request->validated();
$teacherId = (int) (auth()->id() ?? 0);
$data = $this->service->overview(
$teacherId,
isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null,
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students'] ?? []),
'class_section_id' => $data['class_section_id'] ?? null,
'class_section_name' => $data['class_section_name'] ?? null,
'semester' => $data['semester'] ?? null,
'school_year' => $data['school_year'] ?? null,
'scores_locked' => $data['scoresLocked'] ?? false,
]);
}
public function lock(ScoreLockRequest $request): JsonResponse
{
$payload = $request->validated();
$this->service->submitScoresLock(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function studentScores(ScoreStudentRequest $request): JsonResponse
{
$payload = $request->validated();
$parentId = (int) (auth()->id() ?? 0);
$data = $this->service->viewStudentScores($parentId, (string) $payload['school_year']);
return response()->json(['ok' => true] + $data);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScorePredictorRequest;
use App\Http\Resources\Scores\ScorePredictorStudentResource;
use App\Services\Scores\ScorePredictorService;
use Illuminate\Http\JsonResponse;
class ScorePredictorController extends BaseApiController
{
public function __construct(private ScorePredictorService $service)
{
parent::__construct();
}
public function index(ScorePredictorRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->report(
$payload['school_year'] ?? null,
isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null
);
return response()->json([
'ok' => true,
'students' => ScorePredictorStudentResource::collection($data['students'] ?? []),
'school_year' => $data['school_year'] ?? null,
'class_sections' => $data['class_sections'] ?? [],
'selected_class_section_id' => $data['selected_class_section_id'] ?? null,
'semester' => $data['semester'] ?? null,
]);
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Api\System;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Semesters\SchoolYearRangeRequest;
use App\Http\Requests\Semesters\SemesterNormalizeRequest;
use App\Http\Requests\Semesters\SemesterRangeRequest;
use App\Http\Requests\Semesters\SemesterResolveRequest;
use App\Http\Resources\Semesters\SemesterRangeResource;
use App\Http\Resources\Semesters\SemesterResolveResource;
use App\Services\SemesterRangeService;
use Illuminate\Http\JsonResponse;
class SemesterRangeController extends BaseApiController
{
public function __construct(private SemesterRangeService $service)
{
parent::__construct();
}
public function schoolYearRange(SchoolYearRangeRequest $request): JsonResponse
{
$payload = $request->validated();
[$start, $end] = $this->service->getSchoolYearRange($payload['school_year']);
return response()->json([
'ok' => true,
'school_year' => $payload['school_year'],
'range' => new SemesterRangeResource([
'start_date' => $start,
'end_date' => $end,
]),
]);
}
public function semesterRange(SemesterRangeRequest $request): JsonResponse
{
$payload = $request->validated();
$range = $this->service->getSemesterRange($payload['school_year'], $payload['semester']);
if ($range === null) {
return response()->json(['ok' => false, 'message' => 'Invalid semester range.'], 422);
}
return response()->json([
'ok' => true,
'school_year' => $payload['school_year'],
'semester' => $this->service->normalizeSemester($payload['semester']),
'range' => new SemesterRangeResource([
'start_date' => $range[0],
'end_date' => $range[1],
]),
]);
}
public function normalize(SemesterNormalizeRequest $request): JsonResponse
{
$payload = $request->validated();
return response()->json([
'ok' => true,
'normalized' => $this->service->normalizeSemester($payload['semester']),
]);
}
public function resolve(SemesterResolveRequest $request): JsonResponse
{
$payload = $request->validated();
$semester = $this->service->getSemesterForDate($payload['date'] ?? null);
return response()->json([
'ok' => true,
'resolution' => new SemesterResolveResource([
'date' => $payload['date'] ?? null,
'semester' => $semester,
]),
]);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Users\LoginActivityRequest;
use App\Http\Requests\Users\UserListRequest;
use App\Http\Requests\Users\UserStoreRequest;
use App\Http\Requests\Users\UserUpdateRequest;
use App\Http\Resources\Users\LoginActivityResource;
use App\Http\Resources\Users\UserWithRolesResource;
use App\Services\Users\LoginActivityService;
use App\Services\Users\UserListService;
use App\Services\Users\UserManagementService;
use Illuminate\Http\JsonResponse;
class UserController extends BaseApiController
{
public function __construct(
private UserListService $listService,
private UserManagementService $managementService,
private LoginActivityService $loginActivityService
) {
parent::__construct();
}
public function index(UserListRequest $request): JsonResponse
{
$payload = $request->validated();
$users = $this->listService->list($payload['sort'] ?? null, $payload['order'] ?? null);
return response()->json([
'ok' => true,
'users' => UserWithRolesResource::collection($users),
]);
}
public function store(UserStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->managementService->create($payload, (int) (auth()->id() ?? 0));
if (empty($result['ok'])) {
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Failed to create user.',
], 422);
}
return response()->json([
'ok' => true,
'user_id' => $result['user']->id ?? null,
], 201);
}
public function update(UserUpdateRequest $request, int $userId): JsonResponse
{
$payload = $request->validated();
$result = $this->managementService->update($userId, $payload);
if (empty($result['ok'])) {
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Failed to update user.',
], 422);
}
return response()->json([
'ok' => true,
]);
}
public function destroy(int $userId): JsonResponse
{
$result = $this->managementService->delete($userId);
if (empty($result['ok'])) {
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Failed to delete user.',
], 404);
}
return response()->json([
'ok' => true,
]);
}
public function loginActivity(LoginActivityRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->loginActivityService->list(
(int) ($payload['per_page'] ?? 25),
(int) ($payload['page'] ?? 1)
);
return response()->json([
'ok' => true,
'activities' => LoginActivityResource::collection($result['activities']),
'pagination' => $result['pagination'],
]);
}
}