add class progress and fix endpoints

This commit is contained in:
root
2026-03-12 17:27:49 -04:00
parent 0f39dbee62
commit 33be0c9a0d
40 changed files with 2086 additions and 438 deletions
@@ -41,6 +41,11 @@ class AuthController extends BaseApiController
$jwtToken = JWTAuth::fromUser($user);
$sanctumToken = $user->createToken('api')->plainTextToken;
$roles = $user->roles()
->where('roles.is_active', 1)
->pluck('roles.name')
->values()
->all();
return $this->respondSuccess([
'user' => [
@@ -48,6 +53,7 @@ class AuthController extends BaseApiController
'firstname' => $user->firstname,
'lastname' => $user->lastname,
'email' => $user->email,
'roles' => $roles,
],
'jwt' => [
'access_token' => $jwtToken,
@@ -1,53 +0,0 @@
<?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,
]);
}
}
@@ -9,19 +9,30 @@ use App\Http\Requests\ClassPreparation\ClassPreparationMarkPrintedRequest;
use App\Http\Requests\ClassPreparation\ClassPreparationPrintRequest;
use App\Http\Resources\ClassPreparation\ClassPreparationPrintResource;
use App\Http\Resources\ClassPreparation\ClassPreparationResultResource;
use App\Services\ClassPrep\ClassRosterService;
use App\Services\ClassPrep\StickerCountService;
use App\Services\ClassPreparation\ClassPreparationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class ClassPreparationController extends BaseApiController
{
private ClassPreparationService $service;
private StickerCountService $stickerCounts;
private ClassRosterService $roster;
public function __construct(ClassPreparationService $service)
public function __construct(
ClassPreparationService $service,
StickerCountService $stickerCounts,
ClassRosterService $roster
)
{
parent::__construct();
$this->service = $service;
$this->stickerCounts = $stickerCounts;
$this->roster = $roster;
}
public function index(ClassPreparationIndexRequest $request): JsonResponse
@@ -93,4 +104,35 @@ class ClassPreparationController extends BaseApiController
'print' => new ClassPreparationPrintResource($payload),
], 'Class preparation logged.');
}
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,167 @@
<?php
namespace App\Http\Controllers\Api\ClassProgress;
use App\Http\Controllers\Api\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\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\Log;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
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
{
$classId = $request->validated('class_id');
$sundayCount = (int) ($request->validated('sunday_count') ?? 12);
$payload = [
'subject_sections' => $this->metaService->subjectSections(),
'status_options' => $this->metaService->statusOptions(),
'sunday_options' => $this->metaService->sundayOptions($sundayCount),
'curriculum' => $this->metaService->curriculumOptions($classId ? (int) $classId : null),
];
return $this->respondSuccess($payload, 'Progress metadata loaded.');
}
public function index(ClassProgressIndexRequest $request): JsonResponse
{
$filters = $request->validated();
$paginator = $this->queryService->listReports($request->user(), $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()),
'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
{
try {
$filesBySubject = $request->filesBySubject();
$reports = $this->mutationService->createReports($request->user(), $request->validated(), $filesBySubject);
} catch (\InvalidArgumentException $e) {
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
{
$weeklyReports = $this->queryService->weeklyReports($this->laravelRequest->user(), $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));
}
}
@@ -67,6 +67,9 @@ class StudentController extends BaseApiController
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
$validator = Validator::make($data, [
'school_year' => ['nullable', 'string', 'max:50'],
'parent_id' => ['nullable', 'integer', 'min:1'],
'parent_ids' => ['nullable', 'array'],
'parent_ids.*' => ['integer', 'min:1'],
]);
if ($validator->fails()) {
@@ -77,7 +80,16 @@ class StudentController extends BaseApiController
}
$payload = $validator->validated();
$data = $this->directoryService->listStudents($payload['school_year'] ?? null);
$parentIds = [];
if (!empty($payload['parent_ids'])) {
$parentIds = array_values(array_unique(array_map('intval', $payload['parent_ids'])));
}
if (!empty($payload['parent_id'])) {
$parentIds[] = (int) $payload['parent_id'];
}
$parentIds = array_values(array_unique(array_filter($parentIds)));
$data = $this->directoryService->listStudents($payload['school_year'] ?? null, $parentIds ?: null);
return response()->json([
'ok' => true,
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\ClassProgress;
use App\Http\Requests\ApiFormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
abstract class ClassProgressFormRequest extends ApiFormRequest
{
protected function failedValidation(Validator $validator): void
{
throw new HttpResponseException(
response()->json([
'status' => false,
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422)
);
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\ClassProgress;
use App\Http\Requests\ClassProgress\ClassProgressFormRequest;
class ClassProgressIndexRequest extends ClassProgressFormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$statuses = array_keys((array) config('progress.status_options', []));
return [
'class_section_id' => ['nullable', 'integer', 'min:1'],
'teacher_id' => ['nullable', 'integer', 'min:1'],
'week_start' => ['nullable', 'date_format:Y-m-d'],
'week_end' => ['nullable', 'date_format:Y-m-d'],
'subject' => ['nullable', 'string', 'max:120'],
'status' => $statuses ? ['nullable', 'in:' . implode(',', $statuses)] : ['nullable', 'string'],
'group_by_week' => ['nullable', 'boolean'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', 'in:week_start,week_end,subject,status,created_at'],
'sort_dir' => ['nullable', 'in:asc,desc'],
];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\ClassProgress;
use App\Http\Requests\ClassProgress\ClassProgressFormRequest;
class ClassProgressMetaRequest extends ClassProgressFormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'class_id' => ['nullable', 'integer', 'min:1'],
'sunday_count' => ['nullable', 'integer', 'min:1', 'max:52'],
];
}
}
@@ -0,0 +1,54 @@
<?php
namespace App\Http\Requests\ClassProgress;
use App\Http\Requests\ClassProgress\ClassProgressFormRequest;
class ClassProgressStoreRequest extends ClassProgressFormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$rules = [
'class_section_id' => ['required', 'integer', 'exists:classSection,class_section_id'],
'week_start' => ['required', 'date_format:Y-m-d'],
'week_end' => ['nullable', 'date_format:Y-m-d'],
'support_needed' => ['nullable', 'string'],
'flags' => ['nullable', 'array'],
];
$sections = (array) config('progress.subject_sections', []);
$maxFileKb = (int) config('progress.attachments.max_kb', 5120);
$mimes = (array) config('progress.attachments.mimes', ['pdf', 'jpg', 'jpeg', 'png']);
$mimeRule = 'mimes:' . implode(',', $mimes);
foreach ($sections as $slug => $section) {
$rules["covered_{$slug}"] = ['required', 'string'];
$rules["homework_{$slug}"] = ['nullable', 'string'];
$rules["unit_{$slug}"] = ['nullable', 'array'];
$rules["unit_{$slug}.*"] = ['nullable', 'string', 'max:120'];
$rules["chapter_{$slug}"] = ['nullable', 'array'];
$rules["chapter_{$slug}.*"] = ['nullable', 'string', 'max:120'];
$rules["attachment_{$slug}"] = ['nullable', 'array'];
$rules["attachment_{$slug}.*"] = ['nullable', 'file', $mimeRule, 'max:' . $maxFileKb];
}
return $rules;
}
public function filesBySubject(): array
{
$sections = (array) config('progress.subject_sections', []);
$filesBySubject = [];
foreach ($sections as $slug => $section) {
$filesBySubject[$slug] = $this->file("attachment_{$slug}") ?? [];
}
return $filesBySubject;
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\ClassProgress;
use App\Http\Requests\ClassProgress\ClassProgressFormRequest;
class ClassProgressUpdateRequest extends ClassProgressFormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$statuses = array_keys((array) config('progress.status_options', []));
$maxFileKb = (int) config('progress.attachments.max_kb', 5120);
$mimes = (array) config('progress.attachments.mimes', ['pdf', 'jpg', 'jpeg', 'png']);
$mimeRule = 'mimes:' . implode(',', $mimes);
return [
'week_start' => ['nullable', 'date_format:Y-m-d'],
'week_end' => ['nullable', 'date_format:Y-m-d'],
'covered' => ['nullable', 'string'],
'homework' => ['nullable', 'string'],
'unit_title' => ['nullable', 'string', 'max:120'],
'status' => $statuses ? ['nullable', 'in:' . implode(',', $statuses)] : ['nullable', 'string'],
'support_needed' => ['nullable', 'string'],
'flags' => ['nullable', 'array'],
'attachments' => ['nullable', 'array'],
'attachments.*' => ['nullable', 'file', $mimeRule, 'max:' . $maxFileKb],
];
}
public function attachments(): array
{
return $this->file('attachments') ?? [];
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources\ClassProgress;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClassProgressAttachmentResource extends JsonResource
{
public function toArray(Request $request): array
{
$id = $this['id'] ?? $this->id ?? null;
return [
'id' => $id ? (int) $id : null,
'name' => $this['name'] ?? $this->original_name ?? null,
'file_path' => $this['file_path'] ?? $this->file_path ?? null,
'download_url' => $id ? url("/api/v1/class-progress/attachments/{$id}") : null,
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Resources\ClassProgress;
use App\Http\Resources\Admin\Progress\ProgressGroupResource;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ClassProgressGroupCollection extends ResourceCollection
{
public function toArray($request): array
{
return $this->collection->toArray();
}
public static function fromPaginator(LengthAwarePaginator $paginator): array
{
$groups = [];
foreach ($paginator->items() as $report) {
$key = $report->week_start?->format('Y-m-d') ?? '';
if ($key === '') {
continue;
}
if (!isset($groups[$key])) {
$groups[$key] = [
'week_start' => $key,
'week_end' => $report->week_end?->format('Y-m-d'),
'class_section_id' => $report->class_section_id,
'class_section_name' => $report->classSection?->class_section_name ?? '',
'reports' => [],
];
}
$groups[$key]['reports'][$report->subject] = (new ClassProgressReportResource($report))->toArray(request());
}
return [
'items' => ProgressGroupResource::collection(collect(array_values($groups))),
'pagination' => [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'last_page' => $paginator->lastPage(),
],
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Resources\ClassProgress;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClassProgressReportResource extends JsonResource
{
public function toArray(Request $request): array
{
$attachments = $this->attachments ?? $this['attachments'] ?? [];
return [
'id' => $this->id,
'teacher_id' => $this->teacher_id,
'class_section_id' => $this->class_section_id,
'class_section_name' => $this->classSection?->class_section_name,
'week_start' => $this->week_start?->format('Y-m-d'),
'week_end' => $this->week_end?->format('Y-m-d'),
'subject' => $this->subject,
'unit_title' => $this->unit_title,
'covered' => $this->covered,
'homework' => $this->homework,
'status' => $this->status,
'status_label' => $this->status_label ?? (config('progress.status_options')[$this->status] ?? null),
'support_needed' => $this->support_needed,
'flags' => $this->flags_json,
'attachment_path' => $this->attachment_path,
'attachments' => ClassProgressAttachmentResource::collection(collect($attachments)),
'created_at' => $this->created_at?->toIso8601String(),
'updated_at' => $this->updated_at?->toIso8601String(),
];
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Resources\ClassProgress;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ClassProgressShowResource extends JsonResource
{
public function toArray(Request $request): array
{
$weekly = $this['weekly_reports'] ?? [];
return [
'report' => new ClassProgressReportResource($this['report']),
'weekly_reports' => ClassProgressReportResource::collection(collect($weekly)),
'status_options' => $this['status_options'] ?? [],
];
}
}