940afe9319
API CI/CD / Validate (composer + pint) (push) Successful in 2m7s
API CI/CD / Test (PHPUnit) (push) Failing after 2m23s
API CI/CD / Build frontend assets (push) Successful in 2m18s
API CI/CD / Security audit (push) Successful in 31s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
490 lines
17 KiB
PHP
490 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Grading;
|
|
|
|
use App\Http\Controllers\Api\Core\BaseApiController;
|
|
use App\Http\Requests\Grading\AllDecisionsRequest;
|
|
use App\Http\Requests\Grading\BelowSixtyDecisionDetailsRequest;
|
|
use App\Http\Requests\Grading\BelowSixtyDecisionListRequest;
|
|
use App\Http\Requests\Grading\BelowSixtyDecisionSaveRequest;
|
|
use App\Http\Requests\Grading\BelowSixtyDecisionSendRequest;
|
|
use App\Http\Requests\Grading\BelowSixtyEmailRequest;
|
|
use App\Http\Requests\Grading\BelowSixtyListRequest;
|
|
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\GenerateAllDecisionsRequest;
|
|
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\PlacementLevelRequest;
|
|
use App\Http\Requests\Grading\PlacementLevelsRequest;
|
|
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'],
|
|
'class_section_name' => $data['class_section_name'],
|
|
'semester' => $data['semester'],
|
|
'scores_locked' => $data['scores_locked'],
|
|
]);
|
|
}
|
|
|
|
public function update(GradingScoreUpdateRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$this->scoreService->update($payload, $userId);
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
|
|
public function toggleLock(GradingLockRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
if (! $this->currentUserHasAnyRole(['teacher', 'administrator', 'admin'])) {
|
|
return response()->json(['ok' => false, 'message' => 'Forbidden.'], 403);
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$locked = $this->lockService->toggle(
|
|
(int) $payload['class_section_id'],
|
|
(string) $payload['semester'],
|
|
(string) $payload['school_year'],
|
|
$userId
|
|
);
|
|
|
|
return response()->json(['ok' => true, 'locked' => $locked]);
|
|
}
|
|
|
|
public function lockAll(GradingLockAllRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
if (! $this->currentUserHasAnyRole(['teacher', 'administrator', 'admin'])) {
|
|
return response()->json(['ok' => false, 'message' => 'Forbidden.'], 403);
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$count = $this->lockService->lockAll(
|
|
(string) $payload['semester'],
|
|
(string) $payload['school_year'],
|
|
$userId
|
|
);
|
|
|
|
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
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$this->placementService->updatePlacementLevel(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['school_year'],
|
|
$payload['placement_level'] ?? null,
|
|
$userId
|
|
);
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
|
|
public function updatePlacementLevels(PlacementLevelsRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$this->placementService->updatePlacementLevels(
|
|
(int) $payload['class_section_id'],
|
|
(string) $payload['school_year'],
|
|
$payload['placement_level'] ?? [],
|
|
$userId
|
|
);
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
|
|
public function createPlacementBatch(PlacementBatchRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$batchId = $this->placementService->createPlacementBatch(
|
|
(string) $payload['school_year'],
|
|
(string) $payload['placement_test'],
|
|
$payload['placement_level'] ?? [],
|
|
$userId
|
|
);
|
|
|
|
return response()->json(['ok' => true, 'batch_id' => $batchId]);
|
|
}
|
|
|
|
public function showPlacementBatch(int $batchId): JsonResponse
|
|
{
|
|
try {
|
|
$data = $this->placementService->getPlacementBatch($batchId);
|
|
} catch (\RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 404);
|
|
}
|
|
|
|
return response()->json(['ok' => true] + $data);
|
|
}
|
|
|
|
public function updatePlacementBatch(PlacementBatchUpdateRequest $request, int $batchId): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
try {
|
|
$this->placementService->updatePlacementBatch(
|
|
$batchId,
|
|
(string) $payload['school_year'],
|
|
$payload['placement_level'] ?? [],
|
|
$userId
|
|
);
|
|
} catch (\RuntimeException $e) {
|
|
$status = str_contains(strtolower($e->getMessage()), 'not found') ? 404 : 422;
|
|
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], $status);
|
|
}
|
|
|
|
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();
|
|
try {
|
|
$data = $this->belowSixtyService->emailContext(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['school_year'],
|
|
(string) $payload['semester']
|
|
);
|
|
} catch (\RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['ok' => true] + $data);
|
|
}
|
|
|
|
public function sendBelowSixtyEmail(BelowSixtySendRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
try {
|
|
$data = $this->belowSixtyService->emailContext(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['school_year'],
|
|
(string) $payload['semester']
|
|
);
|
|
} catch (\RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
$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
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$this->belowSixtyService->updateStatus(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['semester'],
|
|
(string) $payload['school_year'],
|
|
(string) $payload['status'],
|
|
(string) ($payload['note'] ?? ''),
|
|
$userId
|
|
);
|
|
|
|
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 allDecisions(AllDecisionsRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$data = $this->belowSixtyService->listAllDecisions((string) $payload['school_year']);
|
|
|
|
return response()->json(['ok' => true] + $data);
|
|
}
|
|
|
|
public function generateAllDecisions(GenerateAllDecisionsRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
try {
|
|
$count = $this->belowSixtyService->generateAllDecisions((string) $payload['school_year'], $userId);
|
|
} catch (\RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['ok' => true, 'saved_count' => $count]);
|
|
}
|
|
|
|
public function belowSixtyDecisions(BelowSixtyDecisionListRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$rows = $this->belowSixtyService->listDecisionRows((string) $payload['school_year']);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'rows' => $rows,
|
|
'school_year' => $payload['school_year'],
|
|
'semester' => 'year',
|
|
]);
|
|
}
|
|
|
|
public function saveBelowSixtyDecision(BelowSixtyDecisionSaveRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$result = $this->belowSixtyService->saveDecision(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['school_year'],
|
|
(string) ($payload['decision'] ?? ''),
|
|
(string) ($payload['notes'] ?? ''),
|
|
$userId
|
|
);
|
|
|
|
return response()->json(['ok' => true, 'decision' => $result]);
|
|
}
|
|
|
|
public function belowSixtyDecisionDetails(BelowSixtyDecisionDetailsRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$data = $this->belowSixtyService->studentDecisionDetails(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['school_year']
|
|
);
|
|
|
|
return response()->json(['ok' => true] + $data);
|
|
}
|
|
|
|
public function belowSixtyDecisionEmail(BelowSixtyDecisionDetailsRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
try {
|
|
$data = $this->belowSixtyService->decisionEmailContext(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['school_year']
|
|
);
|
|
} catch (\RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['ok' => true] + $data);
|
|
}
|
|
|
|
public function sendBelowSixtyDecisionEmail(BelowSixtyDecisionSendRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
try {
|
|
$this->belowSixtyService->sendDecisionEmail(
|
|
(int) $payload['student_id'],
|
|
(string) $payload['school_year'],
|
|
isset($payload['subject']) ? (string) $payload['subject'] : null,
|
|
isset($payload['html']) ? (string) $payload['html'] : null
|
|
);
|
|
} catch (\RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
|
|
public function saveBelowSixtyMeeting(BelowSixtyMeetingSaveRequest $request): JsonResponse
|
|
{
|
|
$userId = $this->authenticatedUserIdOrUnauthorized();
|
|
if ($userId instanceof JsonResponse) {
|
|
return $userId;
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$this->belowSixtyService->saveMeeting($payload, $userId);
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
|
|
private function authenticatedUserIdOrUnauthorized(): int|JsonResponse
|
|
{
|
|
$userId = (int) (auth()->id() ?? 0);
|
|
if ($userId <= 0) {
|
|
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
|
|
}
|
|
|
|
return $userId;
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $roles
|
|
*/
|
|
private function currentUserHasAnyRole(array $roles): bool
|
|
{
|
|
$user = request()->user() ?? auth()->user();
|
|
if (! $user || ! method_exists($user, 'roles')) {
|
|
return false;
|
|
}
|
|
|
|
$roles = array_map('strtolower', $roles);
|
|
|
|
return $user->roles()
|
|
->where(function ($query) use ($roles) {
|
|
$query->whereIn('roles.name', $roles)
|
|
->orWhereIn('roles.slug', $roles);
|
|
})
|
|
->exists();
|
|
}
|
|
}
|