add class progress and fix endpoints
This commit is contained in:
+12
-11
@@ -8,12 +8,9 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'status_options' => [
|
||||
// Keep these aligned with your existing values in DB
|
||||
'draft' => 'Draft',
|
||||
'submitted' => 'Submitted',
|
||||
'reviewed' => 'Reviewed',
|
||||
'approved' => 'Approved',
|
||||
'rejected' => 'Rejected',
|
||||
'on_track' => 'On track',
|
||||
'slightly_behind' => 'Slightly behind',
|
||||
'behind' => 'Behind',
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -24,10 +21,14 @@ return [
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'subject_sections' => [
|
||||
// Example placeholders only:
|
||||
// 'quran' => ['Memorization', 'Reading', 'Tajweed'],
|
||||
// 'islamic_studies' => ['Aqeedah', 'Fiqh', 'Seerah'],
|
||||
// 'arabic' => ['Reading', 'Writing', 'Vocabulary'],
|
||||
'islamic' => [
|
||||
'label' => 'Islamic Studies',
|
||||
'db_subject' => 'Islamic Studies',
|
||||
],
|
||||
'quran' => [
|
||||
'label' => 'Quran/Arabic',
|
||||
'db_subject' => 'Quran/Arabic',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
|
||||
@@ -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'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class ClassProgressAttachment extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'class_progress_attachments';
|
||||
|
||||
// CI: useTimestamps = false
|
||||
@@ -31,4 +33,4 @@ class ClassProgressAttachment extends BaseModel
|
||||
// replace ClassProgressReport::class with your actual report model if different
|
||||
return $this->belongsTo(ClassProgressReport::class, 'report_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class ClassProgressReport extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'class_progress_reports';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
@@ -48,7 +50,7 @@ class ClassProgressReport extends BaseModel
|
||||
/* Optional relationships */
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id', 'class_section_id');
|
||||
}
|
||||
|
||||
public function teacher()
|
||||
@@ -60,4 +62,4 @@ class ClassProgressReport extends BaseModel
|
||||
{
|
||||
return $this->hasMany(ClassProgressAttachment::class, 'report_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Models\User;
|
||||
|
||||
class ClassProgressReportPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user !== null;
|
||||
}
|
||||
|
||||
public function view(User $user, ClassProgressReport $report): bool
|
||||
{
|
||||
return $this->owns($user, $report) || $this->isAdmin($user);
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user !== null;
|
||||
}
|
||||
|
||||
public function update(User $user, ClassProgressReport $report): bool
|
||||
{
|
||||
return $this->owns($user, $report) || $this->isAdmin($user);
|
||||
}
|
||||
|
||||
public function delete(User $user, ClassProgressReport $report): bool
|
||||
{
|
||||
return $this->owns($user, $report) || $this->isAdmin($user);
|
||||
}
|
||||
|
||||
private function owns(User $user, ClassProgressReport $report): bool
|
||||
{
|
||||
return (int) $report->teacher_id === (int) $user->id;
|
||||
}
|
||||
|
||||
private function isAdmin(User $user): bool
|
||||
{
|
||||
$roles = $user->roles()
|
||||
->pluck('roles.name')
|
||||
->map(fn ($name) => strtolower((string) $name))
|
||||
->toArray();
|
||||
|
||||
foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) {
|
||||
if (in_array($role, $roles, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use App\Models\Preferences;
|
||||
use App\Models\Staff;
|
||||
use App\Models\SupportRequest;
|
||||
use App\Models\Setting;
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Policies\NavItemPolicy;
|
||||
use App\Policies\ClassSectionPolicy;
|
||||
use App\Policies\IpAttemptPolicy;
|
||||
@@ -30,6 +31,7 @@ use App\Policies\PreferencesPolicy;
|
||||
use App\Policies\StaffPolicy;
|
||||
use App\Policies\SupportRequestPolicy;
|
||||
use App\Policies\SettingPolicy;
|
||||
use App\Policies\ClassProgressReportPolicy;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Routing\Redirector;
|
||||
@@ -76,6 +78,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
Gate::policy(Staff::class, StaffPolicy::class);
|
||||
Gate::policy(SupportRequest::class, SupportRequestPolicy::class);
|
||||
Gate::policy(Setting::class, SettingPolicy::class);
|
||||
Gate::policy(ClassProgressReport::class, ClassProgressReportPolicy::class);
|
||||
|
||||
$this->app->resolving(FormRequest::class, function (FormRequest $request, $app): void {
|
||||
$request->setContainer($app)->setRedirector($app->make(Redirector::class));
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassProgress;
|
||||
|
||||
use App\Models\ClassProgressAttachment;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ClassProgressAttachmentService
|
||||
{
|
||||
public function storeAttachments(int $reportId, array $files): array
|
||||
{
|
||||
if ($files === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stored = [];
|
||||
foreach ($files as $file) {
|
||||
if (!$file instanceof UploadedFile || !$file->isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->storeAttachment($file);
|
||||
$stored[] = [
|
||||
'report_id' => $reportId,
|
||||
'file_path' => $path,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $file->getClientMimeType(),
|
||||
'file_size' => $file->getSize(),
|
||||
'created_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
if ($stored !== []) {
|
||||
ClassProgressAttachment::query()->insert($stored);
|
||||
}
|
||||
|
||||
return $stored;
|
||||
}
|
||||
|
||||
public function resolvePath(string $path): ?string
|
||||
{
|
||||
$path = trim($path);
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$candidates = [];
|
||||
if (str_starts_with($path, 'storage/')) {
|
||||
$relative = ltrim(substr($path, strlen('storage/')), '/');
|
||||
$candidates[] = storage_path('app/public/' . $relative);
|
||||
}
|
||||
|
||||
if (str_starts_with($path, 'writable/uploads/')) {
|
||||
$relative = ltrim(substr($path, strlen('writable/uploads/')), '/');
|
||||
$candidates[] = storage_path('app/' . $relative);
|
||||
$candidates[] = storage_path('app/public/' . $relative);
|
||||
}
|
||||
|
||||
$candidates[] = storage_path('app/' . ltrim($path, '/'));
|
||||
$candidates[] = storage_path('app/public/' . ltrim($path, '/'));
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (is_file($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function storeAttachment(UploadedFile $file): string
|
||||
{
|
||||
$disk = (string) config('progress.attachments.disk', 'public');
|
||||
$directory = (string) config('progress.attachments.directory', 'class_material');
|
||||
|
||||
try {
|
||||
$path = $file->store($directory, $disk);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to store class progress attachment.', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return 'storage/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassProgress;
|
||||
|
||||
use App\Models\SubjectCurriculum;
|
||||
|
||||
class ClassProgressMetaService
|
||||
{
|
||||
public function subjectSections(): array
|
||||
{
|
||||
return (array) config('progress.subject_sections', []);
|
||||
}
|
||||
|
||||
public function statusOptions(): array
|
||||
{
|
||||
return (array) config('progress.status_options', []);
|
||||
}
|
||||
|
||||
public function sundayOptions(int $count = 12): array
|
||||
{
|
||||
$start = now();
|
||||
if ((int) $start->format('w') !== 0) {
|
||||
$start = $start->next('Sunday');
|
||||
}
|
||||
|
||||
$options = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$options[] = $start->format('Y-m-d');
|
||||
$start = $start->addDays(7);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function curriculumOptions(?int $classId): array
|
||||
{
|
||||
if (!$classId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$options = [];
|
||||
foreach ($this->subjectSections() as $slug => $section) {
|
||||
$options[$slug] = SubjectCurriculum::getOptionsForClass((int) $classId, (string) $slug);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassProgress;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ClassProgressMutationService
|
||||
{
|
||||
public function __construct(
|
||||
private ClassProgressRuleService $rules,
|
||||
private ClassProgressAttachmentService $attachments
|
||||
) {}
|
||||
|
||||
public function createReports(User $user, array $payload, array $filesBySubject): Collection
|
||||
{
|
||||
$subjectSections = (array) config('progress.subject_sections', []);
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
|
||||
$this->assertTeacherAssignment($user, $classSectionId);
|
||||
|
||||
$weekStart = (string) ($payload['week_start'] ?? '');
|
||||
$weekEnd = $this->rules->ensureWeekEnd($weekStart, $payload['week_end'] ?? null);
|
||||
|
||||
if (!$this->rules->isWeekEndValid($weekStart, $weekEnd)) {
|
||||
throw new \InvalidArgumentException('Week end must be the same as or after the week start.');
|
||||
}
|
||||
|
||||
$reports = DB::transaction(function () use ($user, $payload, $subjectSections, $classSectionId, $weekStart, $weekEnd, $filesBySubject) {
|
||||
$created = collect();
|
||||
|
||||
foreach ($subjectSections as $slug => $section) {
|
||||
$covered = trim((string) ($payload["covered_{$slug}"] ?? ''));
|
||||
if ($covered === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$homework = trim((string) ($payload["homework_{$slug}"] ?? ''));
|
||||
$unitValues = (array) ($payload["unit_{$slug}"] ?? []);
|
||||
$chapterValues = (array) ($payload["chapter_{$slug}"] ?? []);
|
||||
$unitTitle = $this->rules->buildUnitChapterSummary($unitValues, $chapterValues);
|
||||
|
||||
$report = ClassProgressReport::query()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => $classSectionId,
|
||||
'week_start' => $weekStart,
|
||||
'week_end' => $weekEnd,
|
||||
'subject' => $section['db_subject'] ?? $section['label'] ?? $slug,
|
||||
'unit_title' => $unitTitle,
|
||||
'covered' => $covered,
|
||||
'homework' => $homework !== '' ? $homework : null,
|
||||
'status' => $this->rules->defaultStatus(),
|
||||
'support_needed' => $payload['support_needed'] ?? null,
|
||||
'flags_json' => $this->rules->normalizeFlags($payload['flags'] ?? null),
|
||||
]);
|
||||
|
||||
$attachments = $filesBySubject[$slug] ?? [];
|
||||
$stored = $this->attachments->storeAttachments($report->id, $attachments);
|
||||
if ($stored !== []) {
|
||||
$report->update(['attachment_path' => $stored[0]['file_path'] ?? null]);
|
||||
}
|
||||
|
||||
$created->push($report);
|
||||
}
|
||||
|
||||
if ($created->isEmpty()) {
|
||||
throw new \InvalidArgumentException('Please provide progress for at least one subject.');
|
||||
}
|
||||
|
||||
return $created;
|
||||
});
|
||||
|
||||
Log::info('Class progress reports created.', [
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => $classSectionId,
|
||||
'count' => $reports->count(),
|
||||
]);
|
||||
|
||||
return $reports;
|
||||
}
|
||||
|
||||
public function updateReport(ClassProgressReport $report, array $payload, array $attachments): ClassProgressReport
|
||||
{
|
||||
return DB::transaction(function () use ($report, $payload, $attachments) {
|
||||
$weekStart = $payload['week_start'] ?? $report->week_start?->format('Y-m-d');
|
||||
$weekEnd = $this->rules->ensureWeekEnd((string) $weekStart, $payload['week_end'] ?? null);
|
||||
if ($weekStart && $weekEnd && !$this->rules->isWeekEndValid((string) $weekStart, (string) $weekEnd)) {
|
||||
throw new \InvalidArgumentException('Week end must be the same as or after the week start.');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
if ($weekStart) {
|
||||
$data['week_start'] = $weekStart;
|
||||
}
|
||||
if ($weekEnd) {
|
||||
$data['week_end'] = $weekEnd;
|
||||
}
|
||||
if (array_key_exists('covered', $payload)) {
|
||||
$data['covered'] = $payload['covered'];
|
||||
}
|
||||
if (array_key_exists('homework', $payload)) {
|
||||
$data['homework'] = $payload['homework'];
|
||||
}
|
||||
if (array_key_exists('unit_title', $payload)) {
|
||||
$data['unit_title'] = $payload['unit_title'];
|
||||
}
|
||||
if (array_key_exists('status', $payload)) {
|
||||
$data['status'] = $payload['status'];
|
||||
}
|
||||
if (array_key_exists('support_needed', $payload)) {
|
||||
$data['support_needed'] = $payload['support_needed'];
|
||||
}
|
||||
if (array_key_exists('flags', $payload)) {
|
||||
$data['flags_json'] = $this->rules->normalizeFlags($payload['flags']);
|
||||
}
|
||||
|
||||
if ($data !== []) {
|
||||
$report->fill($data);
|
||||
$report->save();
|
||||
}
|
||||
|
||||
if ($attachments !== []) {
|
||||
$stored = $this->attachments->storeAttachments($report->id, $attachments);
|
||||
if ($stored !== []) {
|
||||
$report->update(['attachment_path' => $stored[0]['file_path'] ?? $report->attachment_path]);
|
||||
}
|
||||
}
|
||||
|
||||
return $report->fresh(['classSection', 'attachments']);
|
||||
});
|
||||
}
|
||||
|
||||
public function deleteReport(ClassProgressReport $report): void
|
||||
{
|
||||
DB::transaction(function () use ($report) {
|
||||
$report->attachments()->delete();
|
||||
$report->delete();
|
||||
});
|
||||
}
|
||||
|
||||
private function assertTeacherAssignment(User $user, int $classSectionId): void
|
||||
{
|
||||
if ($classSectionId <= 0) {
|
||||
throw new \InvalidArgumentException('No class assignment found for this report.');
|
||||
}
|
||||
|
||||
if ($this->isAdmin($user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$assigned = TeacherClass::query()
|
||||
->where('teacher_id', $user->id)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->exists();
|
||||
|
||||
if (!$assigned) {
|
||||
throw new \InvalidArgumentException('No class assignment found for this report.');
|
||||
}
|
||||
}
|
||||
|
||||
private function isAdmin(User $user): bool
|
||||
{
|
||||
$roles = $user->roles()
|
||||
->pluck('roles.name')
|
||||
->map(fn ($name) => strtolower((string) $name))
|
||||
->toArray();
|
||||
|
||||
foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) {
|
||||
if (in_array($role, $roles, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassProgress;
|
||||
|
||||
use App\Models\ClassProgressAttachment;
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class ClassProgressQueryService
|
||||
{
|
||||
public function __construct(
|
||||
private ClassProgressRuleService $rules
|
||||
) {}
|
||||
|
||||
public function listReports(User $user, array $filters): LengthAwarePaginator
|
||||
{
|
||||
$query = ClassProgressReport::query()
|
||||
->with('classSection')
|
||||
->orderBy($filters['sort_by'] ?? 'week_start', $filters['sort_dir'] ?? 'desc');
|
||||
|
||||
if (!$this->isAdmin($user)) {
|
||||
$query->where('teacher_id', $user->id);
|
||||
} elseif (!empty($filters['teacher_id'])) {
|
||||
$query->where('teacher_id', (int) $filters['teacher_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['class_section_id'])) {
|
||||
$query->where('class_section_id', (int) $filters['class_section_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['subject'])) {
|
||||
$query->where('subject', (string) $filters['subject']);
|
||||
}
|
||||
|
||||
if (!empty($filters['status'])) {
|
||||
$query->where('status', (string) $filters['status']);
|
||||
}
|
||||
|
||||
if (!empty($filters['week_start'])) {
|
||||
$query->whereDate('week_start', '>=', $filters['week_start']);
|
||||
}
|
||||
|
||||
if (!empty($filters['week_end'])) {
|
||||
$query->whereDate('week_end', '<=', $filters['week_end']);
|
||||
}
|
||||
|
||||
$perPage = (int) ($filters['per_page'] ?? 20);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getReport(User $user, int $reportId): ClassProgressReport
|
||||
{
|
||||
$query = ClassProgressReport::query()->with('classSection');
|
||||
if (!$this->isAdmin($user)) {
|
||||
$query->where('teacher_id', $user->id);
|
||||
}
|
||||
|
||||
return $query->findOrFail($reportId);
|
||||
}
|
||||
|
||||
public function weeklyReports(User $user, ClassProgressReport $report): array
|
||||
{
|
||||
$query = ClassProgressReport::query()
|
||||
->with('classSection')
|
||||
->where('class_section_id', $report->class_section_id)
|
||||
->whereDate('week_start', $report->week_start)
|
||||
->orderBy('subject', 'asc');
|
||||
|
||||
if (!$this->isAdmin($user)) {
|
||||
$query->where('teacher_id', $user->id);
|
||||
}
|
||||
|
||||
$rows = $query->get();
|
||||
$attachments = $this->attachmentMap($rows->pluck('id')->all());
|
||||
|
||||
return $rows->map(function (ClassProgressReport $row) use ($attachments) {
|
||||
$row->setAttribute('status_label', $this->statusLabel($row->status));
|
||||
$row->setAttribute('attachments', $attachments[$row->id] ?? []);
|
||||
return $row;
|
||||
})->all();
|
||||
}
|
||||
|
||||
public function attachmentMap(array $reportIds): array
|
||||
{
|
||||
$reportIds = array_values(array_filter(array_map('intval', $reportIds)));
|
||||
if ($reportIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = ClassProgressAttachment::query()
|
||||
->whereIn('report_id', $reportIds)
|
||||
->orderBy('id', 'asc')
|
||||
->get();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$map[$row->report_id][] = [
|
||||
'id' => $row->id,
|
||||
'name' => $row->original_name ?: basename((string) $row->file_path),
|
||||
'file_path' => $row->file_path,
|
||||
];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function teacherAssignments(User $user, ?string $schoolYear, ?string $semester): array
|
||||
{
|
||||
if ($schoolYear === null || $schoolYear === '') {
|
||||
$schoolYear = (string) (config('school.school_year') ?? '');
|
||||
}
|
||||
|
||||
return TeacherClass::getClassAssignmentsByUserId($user->id, $schoolYear, $semester);
|
||||
}
|
||||
|
||||
private function statusLabel(?string $status): string
|
||||
{
|
||||
$options = (array) config('progress.status_options', []);
|
||||
return $options[$status] ?? 'Unknown';
|
||||
}
|
||||
|
||||
private function isAdmin(User $user): bool
|
||||
{
|
||||
$roles = $user->roles()
|
||||
->pluck('roles.name')
|
||||
->map(fn ($name) => strtolower((string) $name))
|
||||
->toArray();
|
||||
|
||||
foreach (['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $role) {
|
||||
if (in_array($role, $roles, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassProgress;
|
||||
|
||||
class ClassProgressRuleService
|
||||
{
|
||||
public function defaultStatus(): string
|
||||
{
|
||||
return 'on_track';
|
||||
}
|
||||
|
||||
public function normalizeFlags($flags): ?array
|
||||
{
|
||||
$values = array_values(array_filter((array) $flags, static fn ($item) => $item !== '' && $item !== null));
|
||||
return $values === [] ? null : $values;
|
||||
}
|
||||
|
||||
public function buildUnitChapterSummary(array $unitValues, array $chapterValues): ?string
|
||||
{
|
||||
$parts = [];
|
||||
$count = max(count($unitValues), count($chapterValues));
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$unit = trim((string) ($unitValues[$i] ?? ''));
|
||||
$chapter = trim((string) ($chapterValues[$i] ?? ''));
|
||||
if ($unit === '' && $chapter === '') {
|
||||
continue;
|
||||
}
|
||||
$segment = $unit;
|
||||
if ($chapter !== '') {
|
||||
$segment = $segment !== '' ? $segment . ' / ' . $chapter : $chapter;
|
||||
}
|
||||
if ($segment === '') {
|
||||
continue;
|
||||
}
|
||||
$parts[] = $segment;
|
||||
}
|
||||
|
||||
if ($parts === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$summary = implode(' ; ', $parts);
|
||||
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
|
||||
}
|
||||
|
||||
public function ensureWeekEnd(string $weekStart, ?string $weekEnd): string
|
||||
{
|
||||
if ($weekEnd) {
|
||||
return $weekEnd;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new \DateTime($weekStart);
|
||||
$dt->modify('+6 days');
|
||||
return $dt->format('Y-m-d');
|
||||
} catch (\Exception $e) {
|
||||
return $weekStart;
|
||||
}
|
||||
}
|
||||
|
||||
public function isWeekEndValid(string $weekStart, string $weekEnd): bool
|
||||
{
|
||||
return strtotime($weekEnd) >= strtotime($weekStart);
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ class StudentDirectoryService
|
||||
];
|
||||
}
|
||||
|
||||
public function listStudents(?string $schoolYear = null): array
|
||||
public function listStudents(?string $schoolYear = null, ?array $parentIds = null): array
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$selectedYear = $schoolYear ?: (string) ($context['school_year'] ?? '');
|
||||
@@ -189,6 +189,7 @@ class StudentDirectoryService
|
||||
'school_year',
|
||||
])
|
||||
->when($selectedYear !== '', fn ($q) => $q->where('school_year', $selectedYear))
|
||||
->when(!empty($parentIds), fn ($q) => $q->whereIn('parent_id', $parentIds))
|
||||
->orderBy('lastname')
|
||||
->orderBy('firstname')
|
||||
->get()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Progress Report Status Labels
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'status_options' => [
|
||||
'on_track' => 'On track',
|
||||
'slightly_behind' => 'Slightly behind',
|
||||
'behind' => 'Behind',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Subject Sections Mapping
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'subject_sections' => [
|
||||
'islamic' => [
|
||||
'label' => 'Islamic Studies',
|
||||
'db_subject' => 'Islamic Studies',
|
||||
],
|
||||
'quran' => [
|
||||
'label' => 'Quran/Arabic',
|
||||
'db_subject' => 'Quran/Arabic',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Attachment Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'attachments' => [
|
||||
'disk' => 'public',
|
||||
'directory' => 'class_material',
|
||||
'max_kb' => 5120,
|
||||
'mimes' => ['pdf', 'jpg', 'jpeg', 'png'],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\ClassProgressAttachment;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class ClassProgressAttachmentFactory extends Factory
|
||||
{
|
||||
protected $model = ClassProgressAttachment::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'report_id' => 1,
|
||||
'file_path' => 'storage/class_material/sample.pdf',
|
||||
'original_name' => 'sample.pdf',
|
||||
'mime_type' => 'application/pdf',
|
||||
'file_size' => 12345,
|
||||
'created_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ClassProgressReportFactory extends Factory
|
||||
{
|
||||
protected $model = ClassProgressReport::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$weekStart = Carbon::now()->startOfWeek(Carbon::SUNDAY)->startOfDay();
|
||||
|
||||
return [
|
||||
'class_section_id' => 101,
|
||||
'teacher_id' => 1,
|
||||
'week_start' => $weekStart,
|
||||
'week_end' => (clone $weekStart)->addDays(6),
|
||||
'subject' => 'Islamic Studies',
|
||||
'unit_title' => 'Unit 1 / Chapter A',
|
||||
'covered' => 'Lesson overview',
|
||||
'homework' => 'Practice reading',
|
||||
'status' => 'on_track',
|
||||
'support_needed' => null,
|
||||
'flags_json' => ['flag'],
|
||||
'attachment_path' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1271,19 +1271,15 @@ This document lists every API controller alphabetically with any inline route co
|
||||
- `PATCH /api/v1/refunds/{id}` – Update status, payout amounts/methods, or notes; stamps approval/refunded timestamps when applicable.
|
||||
|
||||
## RegisterController
|
||||
**File:** `app/Http/Controllers/Api/RegisterController.php`
|
||||
**File:** `app/Http/Controllers/Api/Auth/RegisterController.php`
|
||||
**Purpose:** Public registration portal for parents/guests, including CAPTCHA and confirmation email handling. Handles user registration with validation, role assignment, optional second parent information, and email confirmation.
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /api/v1/register` (`index`) – Generate or retrieve CAPTCHA challenge for registration form. Returns:
|
||||
- `captcha_question` – Random CAPTCHA string (4-8 characters) stored in session
|
||||
- `GET /api/v1/auth/register/captcha` (`captcha`) – Generate or retrieve CAPTCHA challenge for registration form. Returns:
|
||||
- `captcha` – Random CAPTCHA string (4-10 characters) stored in session
|
||||
- If CAPTCHA already exists in session, returns existing value
|
||||
- Used to prevent automated registrations
|
||||
- `GET /api/v1/register/success` (`registrationSuccess`) – Get registration success data. Returns:
|
||||
- `email` – Email address from registration session
|
||||
- Returns 400 error if no registration session exists
|
||||
- Used to display confirmation screen after successful registration
|
||||
- `POST /api/v1/register` (`register`) – Submit registration form. Body parameters:
|
||||
- `POST /api/v1/auth/register` (`store`) – Submit registration form. Body parameters:
|
||||
- **Required fields:**
|
||||
- `firstname` (string, required) – First name (2-30 chars, letters/spaces/hyphens only)
|
||||
- `lastname` (string, required) – Last name (2-30 chars, letters/spaces/hyphens only)
|
||||
@@ -1781,7 +1777,7 @@ This document lists every API controller alphabetically with any inline route co
|
||||
**Purpose:** Full REST API for managing students, class assignments, health information, and automated distribution.
|
||||
|
||||
**Endpoints:**
|
||||
- `GET /api/v1/students` (`index`) – Requires auth. Supports pagination and filters (`parent_id`, `class_id`). Augments each record with the student's current class info. Returns paginated list of students.
|
||||
- `GET /api/v1/students` (`index`) – Requires auth. Supports filters (`parent_id`, `parent_ids[]`). Returns list of students for the selected school year.
|
||||
- `GET /api/v1/students/{id}` (`show`) – Requires auth. Returns detailed student profile plus current class section information.
|
||||
- `POST /api/v1/students` (`store`) – Requires auth. Creates a new student. Validates demographic fields (firstname, lastname, dob, gender, parent_id) and automatically stamps school year, semester, and registration date.
|
||||
- `PATCH /api/v1/students/{id}` (`update`) – Requires auth. Updates student information. Accepts fields: `school_id`, `firstname`, `lastname`, `dob` (automatically calculates age), `gender`, `registration_grade`, `photo_consent`, `parent_id`, `registration_date`, `tuition_paid`, `year_of_registration`, `school_year`, `rfid_tag`, `semester`, `is_new`. Also supports health information: `medical_conditions` (comma/semicolon/newline separated list), `allergies` (comma/semicolon/newline separated list), `medical_touched`, `allergies_touched` (flags to indicate user interaction). Health lists are normalized, deduplicated, and synced with related tables. Only updates fields that are provided and non-empty.
|
||||
|
||||
+187
-25
@@ -222,18 +222,69 @@
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Login successful"
|
||||
"example": "Authenticated."
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"example": "eyJ0eXAiOiJKV1QiLCJh..."
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"example": 2
|
||||
},
|
||||
"firstname": {
|
||||
"type": "string",
|
||||
"example": "makamak"
|
||||
},
|
||||
"lastname": {
|
||||
"type": "string",
|
||||
"example": "mkmkmkm"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"example": "mkdfso2123@gmail.com"
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"example": "parent"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer",
|
||||
"example": 3600
|
||||
"jwt": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string",
|
||||
"example": "eyJ0eXAiOiJKV1QiLCJh..."
|
||||
},
|
||||
"token_type": {
|
||||
"type": "string",
|
||||
"example": "bearer"
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer",
|
||||
"example": 216000
|
||||
}
|
||||
}
|
||||
},
|
||||
"sanctum": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string",
|
||||
"example": "2|5NJcqQ8YixX8NIhvKZpGoW1x4BUzhEsshSiIbCFFce48f27f"
|
||||
},
|
||||
"token_type": {
|
||||
"type": "string",
|
||||
"example": "bearer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +292,19 @@
|
||||
},
|
||||
"RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": ["firstname", "lastname", "email", "password", "captcha_answer"],
|
||||
"required": [
|
||||
"firstname",
|
||||
"lastname",
|
||||
"gender",
|
||||
"email",
|
||||
"confirm_email",
|
||||
"cellphone",
|
||||
"city",
|
||||
"state",
|
||||
"zip",
|
||||
"accept_school_policy",
|
||||
"captcha"
|
||||
],
|
||||
"properties": {
|
||||
"firstname": {
|
||||
"type": "string",
|
||||
@@ -251,22 +314,80 @@
|
||||
"type": "string",
|
||||
"example": "Khalil"
|
||||
},
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"example": "Female"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"example": "parent@example.com"
|
||||
},
|
||||
"password": {
|
||||
"confirm_email": {
|
||||
"type": "string",
|
||||
"example": "Str0ngPass!"
|
||||
"format": "email",
|
||||
"example": "parent@example.com"
|
||||
},
|
||||
"phone": {
|
||||
"cellphone": {
|
||||
"type": "string",
|
||||
"example": "1234567890"
|
||||
"example": "203-555-1234"
|
||||
},
|
||||
"captcha_answer": {
|
||||
"address_street": {
|
||||
"type": "string",
|
||||
"example": "42"
|
||||
"example": "123 Main St"
|
||||
},
|
||||
"apt": {
|
||||
"type": "string",
|
||||
"example": "Apt 4B"
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"example": "New Haven"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"example": "CT"
|
||||
},
|
||||
"zip": {
|
||||
"type": "string",
|
||||
"example": "06510"
|
||||
},
|
||||
"accept_school_policy": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"captcha": {
|
||||
"type": "string",
|
||||
"example": "A1B2"
|
||||
},
|
||||
"is_parent": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"no_second_parent_info": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"second_firstname": {
|
||||
"type": "string",
|
||||
"example": "Omar"
|
||||
},
|
||||
"second_lastname": {
|
||||
"type": "string",
|
||||
"example": "Khalil"
|
||||
},
|
||||
"second_gender": {
|
||||
"type": "string",
|
||||
"example": "Male"
|
||||
},
|
||||
"second_email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"example": "second.parent@example.com"
|
||||
},
|
||||
"second_cellphone": {
|
||||
"type": "string",
|
||||
"example": "203-555-4567"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -303,23 +424,37 @@
|
||||
"RegisterResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"ok": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "User registered successfully"
|
||||
},
|
||||
"data": {
|
||||
"registration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserSummary"
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"example": 4321
|
||||
},
|
||||
"message": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "Registration successful. Please check your email for verification."
|
||||
"format": "email",
|
||||
"example": "parent@example.com"
|
||||
},
|
||||
"firstname": {
|
||||
"type": "string",
|
||||
"example": "Yara"
|
||||
},
|
||||
"lastname": {
|
||||
"type": "string",
|
||||
"example": "Khalil"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"example": "parent"
|
||||
},
|
||||
"activation_sent": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1074,7 +1209,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/v1/register": {
|
||||
"/api/v1/auth/register": {
|
||||
"post": {
|
||||
"operationId": "register",
|
||||
"tags": ["Auth"],
|
||||
@@ -1473,6 +1608,16 @@
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parent_ids[]",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "class_id",
|
||||
"in": "query",
|
||||
@@ -4624,6 +4769,23 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parent_id",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parent_ids[]",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
||||
+22
-11
@@ -26,7 +26,7 @@ use App\Http\Controllers\Api\Email\BroadcastEmailController;
|
||||
use App\Http\Controllers\Api\Settings\ConfigurationController;
|
||||
use App\Http\Controllers\Api\Staff\StaffController;
|
||||
use App\Http\Controllers\Api\ClassPreparation\ClassPreparationController;
|
||||
use App\Http\Controllers\Api\ClassPrep\ClassPrepController;
|
||||
use App\Http\Controllers\Api\ClassProgress\ClassProgressController;
|
||||
use App\Http\Controllers\Api\Email\EmailController;
|
||||
use App\Http\Controllers\Api\Email\EmailExtractorController;
|
||||
use App\Http\Controllers\Api\Communication\CommunicationController;
|
||||
@@ -828,8 +828,19 @@ Route::prefix('v1')->group(function () {
|
||||
Route::post('mark-printed', [ClassPreparationController::class, 'markPrinted']);
|
||||
Route::post('adjustments', [ClassPreparationController::class, 'saveAdjustments']);
|
||||
Route::post('print/{classSectionId}/{schoolYear}', [ClassPreparationController::class, 'print']);
|
||||
Route::get('sticker-counts', [ClassPrepController::class, 'stickerCounts']);
|
||||
Route::get('classes/{classSectionId}/students', [ClassPrepController::class, 'studentsByClass']);
|
||||
Route::get('sticker-counts', [ClassPreparationController::class, 'stickerCounts']);
|
||||
Route::get('classes/{classSectionId}/students', [ClassPreparationController::class, 'studentsByClass']);
|
||||
});
|
||||
|
||||
Route::prefix('class-progress')->group(function () {
|
||||
Route::get('meta', [ClassProgressController::class, 'meta']);
|
||||
Route::get('/', [ClassProgressController::class, 'index']);
|
||||
Route::post('/', [ClassProgressController::class, 'store']);
|
||||
Route::get('{class_progress}', [ClassProgressController::class, 'show']);
|
||||
Route::patch('{class_progress}', [ClassProgressController::class, 'update']);
|
||||
Route::delete('{class_progress}', [ClassProgressController::class, 'destroy']);
|
||||
Route::get('{class_progress}/attachment', [ClassProgressController::class, 'downloadLegacyAttachment']);
|
||||
Route::get('attachments/{attachment}', [ClassProgressController::class, 'downloadAttachment']);
|
||||
});
|
||||
|
||||
Route::prefix('badges')->group(function () {
|
||||
@@ -876,12 +887,12 @@ Route::middleware('auth:api')->group(function () {
|
||||
Route::post('log-print', [BadgeController::class, 'logPrint']);
|
||||
});
|
||||
|
||||
Route::prefix('class-prep')->group(function () {
|
||||
Route::get('/', [ClassPreparationController::class, 'index']);
|
||||
Route::post('mark-printed', [ClassPreparationController::class, 'markPrinted']);
|
||||
Route::post('adjustments', [ClassPreparationController::class, 'saveAdjustments']);
|
||||
Route::post('print/{classSectionId}/{schoolYear}', [ClassPreparationController::class, 'print']);
|
||||
Route::get('sticker-counts', [ClassPrepController::class, 'stickerCounts']);
|
||||
Route::get('classes/{classSectionId}/students', [ClassPrepController::class, 'studentsByClass']);
|
||||
});
|
||||
Route::prefix('class-prep')->group(function () {
|
||||
Route::get('/', [ClassPreparationController::class, 'index']);
|
||||
Route::post('mark-printed', [ClassPreparationController::class, 'markPrinted']);
|
||||
Route::post('adjustments', [ClassPreparationController::class, 'saveAdjustments']);
|
||||
Route::post('print/{classSectionId}/{schoolYear}', [ClassPreparationController::class, 'print']);
|
||||
Route::get('sticker-counts', [ClassPreparationController::class, 'stickerCounts']);
|
||||
Route::get('classes/{classSectionId}/students', [ClassPreparationController::class, 'studentsByClass']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\ClassPrep;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassPrepControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_sticker_counts_returns_payload(): void
|
||||
{
|
||||
$this->seedStickerData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-prep/sticker-counts?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'totals' => [
|
||||
'primary' => 6,
|
||||
'secondary' => 1,
|
||||
'students' => 2,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_students_by_class_returns_roster(): void
|
||||
{
|
||||
$this->seedStickerData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-prep/classes/101/students?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertCount(1, $response->json('students'));
|
||||
$this->assertSame('1-A', $response->json('students.0.registration_grade'));
|
||||
}
|
||||
|
||||
private function seedStickerData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'class_name' => 'Class 1',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_name' => 'Class 2',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_name' => 'Class 3',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'class_section_name' => '5-B',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'class_section_name' => 'Youth-1',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
[
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-2',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Two',
|
||||
'age' => 10,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-3',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Three',
|
||||
'age' => 12,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
[
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Admin',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'admin@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->findOrFail(1);
|
||||
}
|
||||
}
|
||||
@@ -12,161 +12,167 @@ class ClassPreparationControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_index_returns_prep_payload(): void
|
||||
public function test_sticker_counts_returns_payload(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
$this->seedStickerData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-prep?school_year=2025-2026&semester=Fall');
|
||||
$response = $this->getJson('/api/v1/class-prep/sticker-counts?school_year=2025-2026&semester=Fall');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'status' => true,
|
||||
'message' => 'Success',
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'schoolYear' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'totals' => [
|
||||
'primary' => 6,
|
||||
'secondary' => 1,
|
||||
'students' => 2,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->assertNotEmpty($response->json('data.results'));
|
||||
}
|
||||
|
||||
public function test_mark_printed_creates_snapshots(): void
|
||||
public function test_students_by_class_returns_roster(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
$this->seedStickerData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/class-prep/mark-printed', [
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'class_section_ids' => ['101'],
|
||||
]);
|
||||
$response = $this->getJson('/api/v1/class-prep/classes/101/students?school_year=2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'status' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('class_preparation_log', [
|
||||
'class_section_id' => 101,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
$this->assertCount(1, $response->json('students'));
|
||||
$this->assertSame('1-A', $response->json('students.0.registration_grade'));
|
||||
}
|
||||
|
||||
public function test_mark_printed_rejects_invalid_payload(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/class-prep/mark-printed', [
|
||||
'class_section_ids' => [],
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonStructure(['message', 'errors']);
|
||||
}
|
||||
|
||||
public function test_save_adjustments_updates_rows(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/class-prep/adjustments', [
|
||||
'class_section_id' => '101',
|
||||
'school_year' => '2025-2026',
|
||||
'adjustments' => [
|
||||
'Small Table' => 2,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson([
|
||||
'status' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('class_prep_adjustments', [
|
||||
'class_section_id' => '101',
|
||||
'school_year' => '2025-2026',
|
||||
'item_name' => 'Small Table',
|
||||
'adjustment' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_print_logs_prep_items(): void
|
||||
{
|
||||
$this->seedPrepData();
|
||||
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/class-prep/print/101/2025-2026');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('status', true);
|
||||
$response->assertJsonPath('data.print.class_section_id', '101');
|
||||
|
||||
$this->assertDatabaseHas('class_preparation_log', [
|
||||
'class_section_id' => 101,
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedPrepData(): void
|
||||
private function seedStickerData(): void
|
||||
{
|
||||
DB::table('configuration')->insert([
|
||||
['id' => 1, 'config_key' => 'school_year', 'config_value' => '2025-2026'],
|
||||
['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'],
|
||||
]);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'class_name' => 'Class 1',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_name' => 'Class 2',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_name' => 'Class 3',
|
||||
'schedule' => 'Sun',
|
||||
'capacity' => 20,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
[
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'class_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'class_section_name' => '5-B',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'class_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'class_section_name' => 'Youth-1',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('students')->insert([
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
[
|
||||
'school_id' => 'S-1',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'One',
|
||||
'age' => 8,
|
||||
'gender' => 'Male',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-2',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Two',
|
||||
'age' => 10,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 0,
|
||||
],
|
||||
[
|
||||
'school_id' => 'S-3',
|
||||
'firstname' => 'Student',
|
||||
'lastname' => 'Three',
|
||||
'age' => 12,
|
||||
'gender' => 'Female',
|
||||
'photo_consent' => 1,
|
||||
'parent_id' => 1,
|
||||
'year_of_registration' => '2025',
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
'is_active' => 1,
|
||||
'is_new' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::table('student_class')->insert([
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
DB::table('inventory_categories')->insert([
|
||||
'type' => 'classroom',
|
||||
'name' => 'Small Table',
|
||||
]);
|
||||
|
||||
DB::table('inventory_items')->insert([
|
||||
'type' => 'classroom',
|
||||
'category_id' => 1,
|
||||
'name' => 'Small Table',
|
||||
'quantity' => 10,
|
||||
'good_qty' => 10,
|
||||
'school_year' => '2025-2026',
|
||||
'semester' => 'Fall',
|
||||
[
|
||||
'student_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 2,
|
||||
'class_section_id' => 102,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
[
|
||||
'student_id' => 3,
|
||||
'class_section_id' => 103,
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\ClassProgress;
|
||||
|
||||
use App\Models\ClassProgressAttachment;
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassProgressControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_meta_returns_sections(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-progress/meta');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('status', true);
|
||||
$this->assertNotEmpty($response->json('data.subject_sections'));
|
||||
}
|
||||
|
||||
public function test_index_returns_paginated_reports(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->seedClassSection();
|
||||
ClassProgressReport::factory()->count(2)->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 101,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-progress');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('status', true);
|
||||
$this->assertCount(2, $response->json('data.items'));
|
||||
}
|
||||
|
||||
public function test_store_creates_reports(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
$user = $this->createUser();
|
||||
$this->seedClassSection();
|
||||
$this->assignTeacher($user->id, 101);
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$payload = [
|
||||
'class_section_id' => 101,
|
||||
'week_start' => '2025-09-07',
|
||||
'week_end' => '2025-09-13',
|
||||
'covered_islamic' => 'Lesson A',
|
||||
'covered_quran' => 'Lesson B',
|
||||
'homework_islamic' => 'HW A',
|
||||
'homework_quran' => 'HW B',
|
||||
'flags' => ['needs_support'],
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/class-progress', $payload);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJsonPath('status', true);
|
||||
$this->assertDatabaseCount('class_progress_reports', 2);
|
||||
}
|
||||
|
||||
public function test_show_returns_weekly_reports(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->seedClassSection();
|
||||
$report = ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 101,
|
||||
'week_start' => '2025-09-07',
|
||||
'week_end' => '2025-09-13',
|
||||
'subject' => 'Islamic Studies',
|
||||
]);
|
||||
ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 101,
|
||||
'week_start' => '2025-09-07',
|
||||
'week_end' => '2025-09-13',
|
||||
'subject' => 'Quran/Arabic',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-progress/' . $report->id);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('status', true);
|
||||
$this->assertCount(2, $response->json('data.weekly_reports'));
|
||||
}
|
||||
|
||||
public function test_update_modifies_report(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->seedClassSection();
|
||||
$report = ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 101,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->patchJson('/api/v1/class-progress/' . $report->id, [
|
||||
'status' => 'behind',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertDatabaseHas('class_progress_reports', [
|
||||
'id' => $report->id,
|
||||
'status' => 'behind',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_destroy_deletes_report(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->seedClassSection();
|
||||
$report = ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 101,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->deleteJson('/api/v1/class-progress/' . $report->id);
|
||||
|
||||
$response->assertOk();
|
||||
$this->assertDatabaseMissing('class_progress_reports', [
|
||||
'id' => $report->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_authorization_blocks_other_teachers(): void
|
||||
{
|
||||
$owner = $this->createUser('teacher1@example.com');
|
||||
$other = $this->createUser('teacher2@example.com');
|
||||
$this->seedClassSection();
|
||||
$report = ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $owner->id,
|
||||
'class_section_id' => 101,
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($other);
|
||||
|
||||
$response = $this->getJson('/api/v1/class-progress/' . $report->id);
|
||||
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
|
||||
public function test_validation_rejects_invalid_payload(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->postJson('/api/v1/class-progress', [
|
||||
'class_section_id' => 'invalid',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
}
|
||||
|
||||
public function test_attachment_download_returns_file(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
Storage::disk('public')->put('class_material/sample.pdf', 'content');
|
||||
|
||||
$user = $this->createUser();
|
||||
$this->seedClassSection();
|
||||
$report = ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 101,
|
||||
]);
|
||||
$attachment = ClassProgressAttachment::factory()->create([
|
||||
'report_id' => $report->id,
|
||||
'file_path' => 'storage/class_material/sample.pdf',
|
||||
]);
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$response = $this->get('/api/v1/class-progress/attachments/' . $attachment->id);
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
private function seedClassSection(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function assignTeacher(int $teacherId, int $classSectionId): void
|
||||
{
|
||||
DB::table('teacher_class')->insert([
|
||||
'class_section_id' => $classSectionId,
|
||||
'teacher_id' => $teacherId,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(string $email = 'teacher@example.com'): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => $email,
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->where('email', $email)->firstOrFail();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Resources\ClassProgress;
|
||||
|
||||
use App\Http\Resources\ClassProgress\ClassProgressReportResource;
|
||||
use App\Models\ClassProgressReport;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassProgressReportResourceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_resource_has_expected_keys(): void
|
||||
{
|
||||
$report = ClassProgressReport::factory()->create();
|
||||
|
||||
$resource = (new ClassProgressReportResource($report))->toArray(request());
|
||||
|
||||
$this->assertArrayHasKey('id', $resource);
|
||||
$this->assertArrayHasKey('subject', $resource);
|
||||
$this->assertArrayHasKey('status', $resource);
|
||||
$this->assertArrayHasKey('attachments', $resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassProgress;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Services\ClassProgress\ClassProgressAttachmentService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassProgressAttachmentServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_store_attachments_persists_records(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
|
||||
$report = ClassProgressReport::factory()->create();
|
||||
$service = new ClassProgressAttachmentService();
|
||||
|
||||
$files = [UploadedFile::fake()->create('sample.pdf', 12)];
|
||||
$stored = $service->storeAttachments($report->id, $files);
|
||||
|
||||
$this->assertCount(1, $stored);
|
||||
$this->assertDatabaseHas('class_progress_attachments', [
|
||||
'report_id' => $report->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassProgress;
|
||||
|
||||
use App\Services\ClassProgress\ClassProgressMetaService;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassProgressMetaServiceTest extends TestCase
|
||||
{
|
||||
public function test_subject_sections_return_configured_values(): void
|
||||
{
|
||||
$service = new ClassProgressMetaService();
|
||||
|
||||
$sections = $service->subjectSections();
|
||||
|
||||
$this->assertArrayHasKey('islamic', $sections);
|
||||
$this->assertArrayHasKey('quran', $sections);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassProgress;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Models\User;
|
||||
use App\Services\ClassProgress\ClassProgressAttachmentService;
|
||||
use App\Services\ClassProgress\ClassProgressMutationService;
|
||||
use App\Services\ClassProgress\ClassProgressRuleService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassProgressMutationServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_create_reports_persists_multiple_subjects(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
$this->seedClassSection();
|
||||
$this->assignTeacher($user->id, 101);
|
||||
|
||||
$service = new ClassProgressMutationService(
|
||||
new ClassProgressRuleService(),
|
||||
new ClassProgressAttachmentService()
|
||||
);
|
||||
|
||||
$payload = [
|
||||
'class_section_id' => 101,
|
||||
'week_start' => '2025-09-07',
|
||||
'week_end' => '2025-09-13',
|
||||
'covered_islamic' => 'Lesson A',
|
||||
'covered_quran' => 'Lesson B',
|
||||
];
|
||||
|
||||
$reports = $service->createReports($user, $payload, []);
|
||||
|
||||
$this->assertCount(2, $reports);
|
||||
$this->assertDatabaseCount('class_progress_reports', 2);
|
||||
}
|
||||
|
||||
public function test_update_report_changes_status(): void
|
||||
{
|
||||
$report = ClassProgressReport::factory()->create([
|
||||
'status' => 'on_track',
|
||||
]);
|
||||
|
||||
$service = new ClassProgressMutationService(
|
||||
new ClassProgressRuleService(),
|
||||
new ClassProgressAttachmentService()
|
||||
);
|
||||
|
||||
$updated = $service->updateReport($report, ['status' => 'behind'], []);
|
||||
|
||||
$this->assertSame('behind', $updated->status);
|
||||
}
|
||||
|
||||
public function test_delete_report_removes_record(): void
|
||||
{
|
||||
$report = ClassProgressReport::factory()->create();
|
||||
$service = new ClassProgressMutationService(
|
||||
new ClassProgressRuleService(),
|
||||
new ClassProgressAttachmentService()
|
||||
);
|
||||
|
||||
$service->deleteReport($report);
|
||||
|
||||
$this->assertDatabaseMissing('class_progress_reports', ['id' => $report->id]);
|
||||
}
|
||||
|
||||
private function seedClassSection(): void
|
||||
{
|
||||
DB::table('classSection')->insert([
|
||||
'id' => 1,
|
||||
'class_id' => 1,
|
||||
'class_section_id' => 101,
|
||||
'class_section_name' => '1-A',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
}
|
||||
|
||||
private function assignTeacher(int $teacherId, int $classSectionId): void
|
||||
{
|
||||
DB::table('teacher_class')->insert([
|
||||
'class_section_id' => $classSectionId,
|
||||
'teacher_id' => $teacherId,
|
||||
'position' => 'main',
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->where('email', 'teacher@example.com')->firstOrFail();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassProgress;
|
||||
|
||||
use App\Models\ClassProgressReport;
|
||||
use App\Models\User;
|
||||
use App\Services\ClassProgress\ClassProgressQueryService;
|
||||
use App\Services\ClassProgress\ClassProgressRuleService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClassProgressQueryServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_list_reports_filters_by_class_section(): void
|
||||
{
|
||||
$user = $this->createUser();
|
||||
ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 101,
|
||||
]);
|
||||
ClassProgressReport::factory()->create([
|
||||
'teacher_id' => $user->id,
|
||||
'class_section_id' => 202,
|
||||
]);
|
||||
|
||||
$service = new ClassProgressQueryService(new ClassProgressRuleService());
|
||||
$results = $service->listReports($user, ['class_section_id' => 101]);
|
||||
|
||||
$this->assertSame(1, $results->total());
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
DB::table('users')->insert([
|
||||
'school_id' => 1,
|
||||
'firstname' => 'Teacher',
|
||||
'lastname' => 'User',
|
||||
'cellphone' => '5555555555',
|
||||
'email' => 'teacher@example.com',
|
||||
'address_street' => '123 Main',
|
||||
'city' => 'City',
|
||||
'state' => 'ST',
|
||||
'zip' => '12345',
|
||||
'accept_school_policy' => 1,
|
||||
'is_verified' => 1,
|
||||
'status' => 'Active',
|
||||
'is_suspended' => 0,
|
||||
'failed_attempts' => 0,
|
||||
'password' => bcrypt('secret'),
|
||||
'semester' => 'Fall',
|
||||
'school_year' => '2025-2026',
|
||||
]);
|
||||
|
||||
return User::query()->where('email', 'teacher@example.com')->firstOrFail();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services\ClassProgress;
|
||||
|
||||
use App\Services\ClassProgress\ClassProgressRuleService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ClassProgressRuleServiceTest extends TestCase
|
||||
{
|
||||
public function test_build_unit_chapter_summary_limits_length(): void
|
||||
{
|
||||
$service = new ClassProgressRuleService();
|
||||
|
||||
$summary = $service->buildUnitChapterSummary(['Unit 1'], ['Chapter A']);
|
||||
|
||||
$this->assertSame('Unit 1 / Chapter A', $summary);
|
||||
}
|
||||
|
||||
public function test_ensure_week_end_defaults_to_six_days(): void
|
||||
{
|
||||
$service = new ClassProgressRuleService();
|
||||
|
||||
$weekEnd = $service->ensureWeekEnd('2025-09-07', null);
|
||||
|
||||
$this->assertSame('2025-09-13', $weekEnd);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user