Files
alrahma_sunday_school_api/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php
T
root 90f9857b06
API CI/CD / Validate (composer + pint) (push) Successful in 3m7s
API CI/CD / Test (PHPUnit) (push) Failing after 5m46s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 49s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
security fix and fix parent pages
2026-07-06 02:14:16 -04:00

301 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers\Api\ClassProgress;
use App\Http\Controllers\Api\Core\BaseApiController;
use App\Http\Requests\ClassProgress\ClassProgressIndexRequest;
use App\Http\Requests\ClassProgress\ClassProgressMetaRequest;
use App\Http\Requests\ClassProgress\ClassProgressStoreRequest;
use App\Http\Requests\ClassProgress\ClassProgressUpdateRequest;
use App\Http\Resources\ClassProgress\ClassProgressGroupCollection;
use App\Http\Resources\ClassProgress\ClassProgressReportResource;
use App\Http\Resources\ClassProgress\ClassProgressShowResource;
use App\Models\ClassProgressAttachment;
use App\Models\ClassProgressReport;
use App\Models\Configuration;
use App\Models\User;
use App\Services\ClassProgress\ClassProgressAttachmentService;
use App\Services\ClassProgress\ClassProgressMetaService;
use App\Services\ClassProgress\ClassProgressMutationService;
use App\Services\ClassProgress\ClassProgressQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
class ClassProgressController extends BaseApiController
{
public function __construct(
private ClassProgressQueryService $queryService,
private ClassProgressMutationService $mutationService,
private ClassProgressMetaService $metaService,
private ClassProgressAttachmentService $attachmentService
) {
parent::__construct();
$this->authorizeResource(ClassProgressReport::class, 'class_progress');
}
public function meta(ClassProgressMetaRequest $request): JsonResponse
{
return $this->respondSuccess(
$this->buildMetaPayload(
$request->validated('class_id'),
(int) ($request->validated('sunday_count') ?? 12),
$request->query('school_year'),
$request->query('semester')
),
'Progress metadata loaded.'
);
}
/**
* Legacy teacher portal URL for the weekly class progress submit page.
*/
public function legacySubmitForm(ClassProgressMetaRequest $request): JsonResponse
{
$auth = $this->requireAuthenticatedUser($request->user());
if ($auth instanceof JsonResponse) {
return $auth;
}
$schoolYear = (string) (Configuration::getConfig('school_year') ?? '');
$semester = (string) (Configuration::getConfig('semester') ?? '');
$assignments = $this->queryService->teacherAssignments($auth, $schoolYear, $semester);
$requestedClassId = (int) ($request->validated('class_id') ?? 0);
$activeClassId = $requestedClassId > 0 ? $requestedClassId : (int) ($assignments[0]['class_id'] ?? 0);
$activeAssignment = collect($assignments)->first(
fn (array $assignment) => (int) ($assignment['class_id'] ?? 0) === $activeClassId
) ?? ($assignments[0] ?? []);
$sundayCount = (int) ($request->validated('sunday_count') ?? 12);
$payload = $this->buildMetaPayload($activeClassId > 0 ? $activeClassId : null, $sundayCount, $schoolYear, $semester);
$payload['classes'] = $assignments;
$payload['defaults'] = [
'class_id' => $activeClassId > 0 ? $activeClassId : null,
'class_section_id' => (int) ($activeAssignment['class_section_id'] ?? 0) ?: null,
'school_year' => $schoolYear !== '' ? $schoolYear : null,
'semester' => $semester !== '' ? $semester : null,
'week_start' => $this->metaService->pickDefaultWeekStart($payload['sunday_options']),
];
return $this->respondSuccess($payload, 'Progress metadata loaded.');
}
public function index(ClassProgressIndexRequest $request): JsonResponse
{
$auth = $this->requireAuthenticatedUser($request->user());
if ($auth instanceof JsonResponse) {
return $auth;
}
if ($this->isLegacyTeacherProgressListRoute($request) && ! $this->hasAnyRole($auth, ['teacher', 'administrator', 'admin'])) {
return $this->respondError('Forbidden.', Response::HTTP_FORBIDDEN);
}
$filters = $request->validated();
$meta = $this->queryService->meta($filters['school_year'] ?? null, $filters['semester'] ?? null);
$filters['school_year'] = $filters['school_year'] ?? ($meta['school_year'] ?? null);
if ($this->isAdminProgressAliasRoute($request)) {
$filters['group_by_week'] = true;
}
$paginator = $this->queryService->listReports($auth, $filters);
if (! empty($filters['group_by_week'])) {
$grouped = ClassProgressGroupCollection::fromPaginator($paginator);
return $this->respondSuccess($grouped, 'Progress reports retrieved.');
}
return $this->respondSuccess([
'items' => ClassProgressReportResource::collection($paginator->items()),
'meta' => $meta,
'pagination' => [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'last_page' => $paginator->lastPage(),
],
], 'Progress reports retrieved.');
}
public function store(ClassProgressStoreRequest $request): JsonResponse
{
$auth = $this->requireAuthenticatedUser($request->user());
if ($auth instanceof JsonResponse) {
return $auth;
}
try {
$filesBySubject = $request->filesBySubject();
$reports = $this->mutationService->createReports($auth, $request->validated(), $filesBySubject);
} catch (\InvalidArgumentException $e) {
if ($e->getMessage() === 'CONFIRM_OVERWRITE') {
return response()->json([
'status' => false,
'message' => 'A progress report already exists for this week. Resubmit with confirm_overwrite=true to replace it.',
'data' => ['requires_confirmation' => true],
], 409);
}
return $this->respondError($e->getMessage(), 422);
} catch (\Throwable $e) {
Log::error('Failed to create class progress reports.', [
'error' => $e->getMessage(),
]);
return $this->respondError('Unable to save progress reports.', 500);
}
return $this->respondCreated(ClassProgressReportResource::collection($reports), 'Progress reports saved.');
}
public function show(ClassProgressReport $class_progress): JsonResponse
{
$auth = $this->requireAuthenticatedUser($this->laravelRequest->user());
if ($auth instanceof JsonResponse) {
return $auth;
}
$weeklyReports = $this->queryService->weeklyReports($auth, $class_progress);
return $this->respondSuccess(
new ClassProgressShowResource([
'report' => $class_progress,
'weekly_reports' => $weeklyReports,
'status_options' => $this->metaService->statusOptions(),
]),
'Progress report retrieved.'
);
}
public function update(ClassProgressUpdateRequest $request, ClassProgressReport $class_progress): JsonResponse
{
try {
$updated = $this->mutationService->updateReport($class_progress, $request->validated(), $request->attachments());
} catch (\InvalidArgumentException $e) {
return $this->respondError($e->getMessage(), 422);
} catch (\Throwable $e) {
Log::error('Failed to update class progress report.', [
'error' => $e->getMessage(),
]);
return $this->respondError('Unable to update progress report.', 500);
}
return $this->respondSuccess(new ClassProgressReportResource($updated), 'Progress report updated.');
}
public function destroy(ClassProgressReport $class_progress): JsonResponse
{
try {
$this->mutationService->deleteReport($class_progress);
} catch (\Throwable $e) {
Log::error('Failed to delete class progress report.', [
'error' => $e->getMessage(),
]);
return $this->respondError('Unable to delete progress report.', 500);
}
return $this->respondDeleted(null, 'Progress report deleted.');
}
public function downloadAttachment(int $attachment): BinaryFileResponse|JsonResponse
{
$record = ClassProgressAttachment::query()->find($attachment);
if (! $record || ! $record->file_path) {
return $this->respondError('Attachment not found.', 404);
}
if ($record->report) {
$this->authorize('view', $record->report);
}
$path = $this->attachmentService->resolvePath($record->file_path);
if (! $path) {
return $this->respondError('Attachment missing.', 404);
}
$downloadName = $record->original_name ?: basename($path);
return response()->download($path, $downloadName);
}
public function downloadLegacyAttachment(ClassProgressReport $class_progress): BinaryFileResponse|JsonResponse
{
$this->authorize('view', $class_progress);
if (! $class_progress->attachment_path) {
return $this->respondError('Attachment not found.', 404);
}
$path = $this->attachmentService->resolvePath($class_progress->attachment_path);
if (! $path) {
return $this->respondError('Attachment missing.', 404);
}
return response()->download($path, basename($path));
}
/**
* @param mixed $user
*/
private function requireAuthenticatedUser($user): User|JsonResponse
{
if (! $user instanceof User) {
$user = auth()->user();
}
if (! $user instanceof User) {
$user = $this->laravelRequest->user();
}
if (! $user instanceof User) {
return $this->respondError('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
return $user;
}
private function isLegacyTeacherProgressListRoute(ClassProgressIndexRequest $request): bool
{
return $request->is('api/v1/teacher/class-progress-history')
|| $request->is('api/v1/teacher/class-progress-view');
}
private function isAdminProgressAliasRoute(ClassProgressIndexRequest $request): bool
{
return $request->is('api/v1/administrator/progress')
|| $request->is('api/v1/admin/progress');
}
/**
* @param list<string> $roles
*/
private function hasAnyRole(User $user, array $roles): bool
{
$roles = array_map('strtolower', $roles);
return $user->roles()
->where(function ($query) use ($roles) {
$query->whereIn(DB::raw('LOWER(name)'), $roles)
->orWhereIn(DB::raw('LOWER(slug)'), $roles);
})
->exists();
}
private function buildMetaPayload(?int $classId, int $sundayCount, ?string $schoolYear = null, ?string $semester = null): array
{
$meta = $this->queryService->meta($schoolYear, $semester);
return [
'subject_sections' => $this->metaService->subjectSections(),
'status_options' => $this->metaService->statusOptions(),
'sunday_options' => $this->metaService->sundayOptionsAlignedWithCi($sundayCount),
'curriculum' => $this->metaService->curriculumOptions($classId, $meta['school_year'] ?? null),
'meta' => $meta,
];
}
}