fix exam and grading
This commit is contained in:
@@ -4,6 +4,10 @@ namespace App\Http\Controllers\Api\Grading;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\Grading\BelowSixtyEmailRequest;
|
||||
use App\Http\Requests\Grading\BelowSixtyDecisionDetailsRequest;
|
||||
use App\Http\Requests\Grading\BelowSixtyDecisionListRequest;
|
||||
use App\Http\Requests\Grading\BelowSixtyDecisionSaveRequest;
|
||||
use App\Http\Requests\Grading\BelowSixtyDecisionSendRequest;
|
||||
use App\Http\Requests\Grading\BelowSixtyMeetingRequest;
|
||||
use App\Http\Requests\Grading\BelowSixtyMeetingSaveRequest;
|
||||
use App\Http\Requests\Grading\BelowSixtySendRequest;
|
||||
@@ -312,6 +316,73 @@ class GradingController extends BaseApiController
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
|
||||
public function belowSixtyDecisions(BelowSixtyDecisionListRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$rows = $this->belowSixtyService->listDecisionRows((string) $payload['school_year']);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'rows' => $rows,
|
||||
'school_year' => $payload['school_year'],
|
||||
'semester' => 'year',
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveBelowSixtyDecision(BelowSixtyDecisionSaveRequest $request): JsonResponse
|
||||
{
|
||||
$userId = $this->authenticatedUserIdOrUnauthorized();
|
||||
if ($userId instanceof JsonResponse) {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$result = $this->belowSixtyService->saveDecision(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year'],
|
||||
(string) ($payload['decision'] ?? ''),
|
||||
(string) ($payload['notes'] ?? ''),
|
||||
$userId
|
||||
);
|
||||
|
||||
return response()->json(['ok' => true, 'decision' => $result]);
|
||||
}
|
||||
|
||||
public function belowSixtyDecisionDetails(BelowSixtyDecisionDetailsRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = $this->belowSixtyService->studentDecisionDetails(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year']
|
||||
);
|
||||
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
|
||||
public function belowSixtyDecisionEmail(BelowSixtyDecisionDetailsRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = $this->belowSixtyService->decisionEmailContext(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year']
|
||||
);
|
||||
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
|
||||
public function sendBelowSixtyDecisionEmail(BelowSixtyDecisionSendRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$this->belowSixtyService->sendDecisionEmail(
|
||||
(int) $payload['student_id'],
|
||||
(string) $payload['school_year'],
|
||||
isset($payload['subject']) ? (string) $payload['subject'] : null,
|
||||
isset($payload['html']) ? (string) $payload['html'] : null
|
||||
);
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function saveBelowSixtyMeeting(BelowSixtyMeetingSaveRequest $request): JsonResponse
|
||||
{
|
||||
$userId = $this->authenticatedUserIdOrUnauthorized();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Grading;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class BelowSixtyDecisionDetailsRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'school_year' => ['required', 'string', 'max:50'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Grading;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class BelowSixtyDecisionListRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'school_year' => ['required', 'string', 'max:50'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Grading;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class BelowSixtyDecisionSaveRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'school_year' => ['required', 'string', 'max:50'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
'decision' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:100',
|
||||
Rule::in([
|
||||
'Pass',
|
||||
'Repeat Class',
|
||||
'Make-up exam in fall',
|
||||
'Deferred decision',
|
||||
'Expel',
|
||||
'Withdrawn',
|
||||
]),
|
||||
],
|
||||
'notes' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Grading;
|
||||
|
||||
use App\Http\Requests\ApiFormRequest;
|
||||
|
||||
class BelowSixtyDecisionSendRequest extends ApiFormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'school_year' => ['required', 'string', 'max:50'],
|
||||
'subject' => ['nullable', 'string', 'max:255'],
|
||||
'html' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,19 @@ class ExamDraftResource extends JsonResource
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($this['id'] ?? 0),
|
||||
'teacher_id' => (int) ($this['teacher_id'] ?? 0),
|
||||
'teacher_id' => (int) (($this['teacher_id'] ?? $this['author_id'] ?? 0) ?: 0),
|
||||
'class_section_id' => (int) ($this['class_section_id'] ?? 0),
|
||||
'class_section_name' => $this['class_section_name'] ?? null,
|
||||
'semester' => $this['semester'] ?? null,
|
||||
'school_year' => $this['school_year'] ?? null,
|
||||
'exam_type' => $this['exam_type'] ?? null,
|
||||
'draft_title' => $this['draft_title'] ?? null,
|
||||
'description' => $this['description'] ?? null,
|
||||
'teacher_file' => $this['teacher_file'] ?? null,
|
||||
'teacher_filename' => $this['teacher_filename'] ?? null,
|
||||
'description' => $this['description'] ?? ($this['author_comment'] ?? null),
|
||||
'teacher_file' => $this['teacher_file'] ?? ($this['author_file'] ?? null),
|
||||
'teacher_filename' => $this['teacher_filename'] ?? ($this['author_filename'] ?? null),
|
||||
'status' => $this['status'] ?? null,
|
||||
'admin_id' => $this['admin_id'] ?? null,
|
||||
'admin_comments' => $this['admin_comments'] ?? null,
|
||||
'admin_id' => $this['admin_id'] ?? ($this['reviewer_id'] ?? null),
|
||||
'admin_comments' => $this['admin_comments'] ?? ($this['reviewer_comments'] ?? null),
|
||||
'reviewed_at' => $this['reviewed_at'] ?? null,
|
||||
'final_file' => $this['final_file'] ?? null,
|
||||
'final_filename' => $this['final_filename'] ?? null,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class BelowSixtyDecision extends BaseModel
|
||||
{
|
||||
protected $table = 'below_sixty_decisions';
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
'decision',
|
||||
'notes',
|
||||
'decided_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'decided_by' => 'integer',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class StudentDecision extends BaseModel
|
||||
{
|
||||
protected $table = 'student_decisions';
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_year',
|
||||
'class_section_name',
|
||||
'year_score',
|
||||
'decision',
|
||||
'source',
|
||||
'notes',
|
||||
'generated_by',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'generated_by' => 'integer',
|
||||
'year_score' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
@@ -17,19 +17,45 @@ class ExamDraftAdminService
|
||||
public function list(): array
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$teacherUserColumn = (string) ($context['teacher_user_column'] ?? '');
|
||||
$adminUserColumn = (string) ($context['admin_user_column'] ?? '');
|
||||
|
||||
$drafts = DB::table('exam_drafts as ed')
|
||||
$draftsQuery = DB::table('exam_drafts as ed')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ed.class_section_id')
|
||||
->leftJoin('users as u', 'u.id', '=', 'ed.teacher_id')
|
||||
->leftJoin('users as a', 'a.id', '=', 'ed.admin_id')
|
||||
->select(
|
||||
'ed.*',
|
||||
'cs.class_section_name',
|
||||
);
|
||||
|
||||
if ($teacherUserColumn !== '') {
|
||||
$draftsQuery
|
||||
->leftJoin('users as u', "u.id", '=', "ed.{$teacherUserColumn}")
|
||||
->addSelect(
|
||||
'u.firstname as teacher_first',
|
||||
'u.lastname as teacher_last',
|
||||
'u.lastname as teacher_last'
|
||||
);
|
||||
} else {
|
||||
$draftsQuery->addSelect(
|
||||
DB::raw('NULL as teacher_first'),
|
||||
DB::raw('NULL as teacher_last')
|
||||
);
|
||||
}
|
||||
|
||||
if ($adminUserColumn !== '') {
|
||||
$draftsQuery
|
||||
->leftJoin('users as a', 'a.id', '=', "ed.{$adminUserColumn}")
|
||||
->addSelect(
|
||||
'a.firstname as admin_first',
|
||||
'a.lastname as admin_last'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$draftsQuery->addSelect(
|
||||
DB::raw('NULL as admin_first'),
|
||||
DB::raw('NULL as admin_last')
|
||||
);
|
||||
}
|
||||
|
||||
$drafts = $draftsQuery
|
||||
->orderByDesc('ed.created_at')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
@@ -88,6 +114,7 @@ class ExamDraftAdminService
|
||||
public function uploadLegacy(int $adminId, array $payload, \Illuminate\Http\UploadedFile $file): array
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$adminUserColumn = (string) ($context['admin_user_column'] ?? '');
|
||||
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
|
||||
$schoolYear = trim((string) ($payload['school_year'] ?? $context['school_year'] ?? ''));
|
||||
$semester = trim((string) ($payload['semester'] ?? $context['semester'] ?? ''));
|
||||
@@ -124,20 +151,35 @@ class ExamDraftAdminService
|
||||
: (pathinfo($stored['original'], PATHINFO_FILENAME) . '.pdf');
|
||||
|
||||
$payload = [
|
||||
'teacher_id' => $adminId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => ucfirst(strtolower($semester)),
|
||||
'school_year' => $schoolYear,
|
||||
'exam_type' => $examType !== '' ? $examType : null,
|
||||
'draft_title' => $examType !== '' ? $examType : 'Legacy Exam',
|
||||
'final_file' => $pdfName,
|
||||
'final_filename' => $finalFilename,
|
||||
'status' => 'finalized',
|
||||
'admin_id' => $adminId,
|
||||
'reviewed_at' => now(),
|
||||
'version' => 1,
|
||||
];
|
||||
|
||||
if ($context['has_teacher_id'] ?? false) {
|
||||
$payload['teacher_id'] = $adminId;
|
||||
}
|
||||
if ($context['has_author_id'] ?? false) {
|
||||
$payload['author_id'] = $adminId;
|
||||
}
|
||||
|
||||
if ($context['has_final_filename'] ?? false) {
|
||||
$payload['final_filename'] = $finalFilename;
|
||||
}
|
||||
if ($context['has_admin_id'] ?? false) {
|
||||
$payload['admin_id'] = $adminId;
|
||||
} elseif ($adminUserColumn === 'reviewer_id') {
|
||||
$payload['reviewer_id'] = $adminId;
|
||||
}
|
||||
if ($context['has_reviewed_at'] ?? false) {
|
||||
$payload['reviewed_at'] = now();
|
||||
}
|
||||
|
||||
if ($context['has_legacy'] ?? false) {
|
||||
$payload['is_legacy'] = 1;
|
||||
}
|
||||
@@ -171,13 +213,26 @@ class ExamDraftAdminService
|
||||
$status = 'reviewed';
|
||||
}
|
||||
|
||||
$context = $this->configService->context();
|
||||
|
||||
$update = [
|
||||
'status' => $status,
|
||||
'admin_comments' => $comments !== '' ? $comments : null,
|
||||
'admin_id' => $adminId,
|
||||
'reviewed_at' => now(),
|
||||
];
|
||||
|
||||
if ($context['has_admin_comments'] ?? false) {
|
||||
$update['admin_comments'] = $comments !== '' ? $comments : null;
|
||||
} elseif ($context['has_reviewer_comments'] ?? false) {
|
||||
$update['reviewer_comments'] = $comments !== '' ? $comments : null;
|
||||
}
|
||||
if ($context['has_admin_id'] ?? false) {
|
||||
$update['admin_id'] = $adminId;
|
||||
} elseif (($context['admin_user_column'] ?? null) === 'reviewer_id') {
|
||||
$update['reviewer_id'] = $adminId;
|
||||
}
|
||||
if ($context['has_reviewed_at'] ?? false) {
|
||||
$update['reviewed_at'] = now();
|
||||
}
|
||||
|
||||
if ($file) {
|
||||
$stored = $this->fileService->storeUploadedFile(
|
||||
$file,
|
||||
@@ -189,7 +244,9 @@ class ExamDraftAdminService
|
||||
return ['ok' => false, 'message' => 'Unable to store the final draft.'];
|
||||
}
|
||||
$update['final_file'] = $stored['stored'];
|
||||
if ($context['has_final_filename'] ?? false) {
|
||||
$update['final_filename'] = $stored['original'];
|
||||
}
|
||||
$pdfName = $this->fileService->ensurePdfExists(
|
||||
$stored['stored'],
|
||||
$stored['extension'],
|
||||
@@ -198,27 +255,29 @@ class ExamDraftAdminService
|
||||
if ($pdfName !== null && ($this->configService->context()['has_final_pdf'] ?? false)) {
|
||||
$update['final_pdf_file'] = $pdfName;
|
||||
}
|
||||
} elseif ($status === 'finalized' && !empty($draft->teacher_file)) {
|
||||
} elseif ($status === 'finalized' && !empty($draft->teacher_file ?: $draft->author_file)) {
|
||||
$copied = $this->fileService->copyDraftToFinal(
|
||||
(string) $draft->teacher_file,
|
||||
(string) ($draft->teacher_file ?? $draft->author_file),
|
||||
ExamDraftConfigService::TEACHER_UPLOAD_DIR,
|
||||
ExamDraftConfigService::FINAL_UPLOAD_DIR
|
||||
);
|
||||
if ($copied !== null) {
|
||||
$update['final_file'] = $copied;
|
||||
$update['final_filename'] = $draft->teacher_filename ?? $draft->teacher_file;
|
||||
if ($context['has_final_filename'] ?? false) {
|
||||
$update['final_filename'] = $draft->teacher_filename ?? $draft->author_filename ?? $draft->teacher_file ?? $draft->author_file;
|
||||
}
|
||||
$pdfName = $this->fileService->ensurePdfExists(
|
||||
$copied,
|
||||
pathinfo($copied, PATHINFO_EXTENSION),
|
||||
ExamDraftConfigService::FINAL_UPLOAD_DIR
|
||||
);
|
||||
if ($pdfName !== null && ($this->configService->context()['has_final_pdf'] ?? false)) {
|
||||
if ($pdfName !== null && ($context['has_final_pdf'] ?? false)) {
|
||||
$update['final_pdf_file'] = $pdfName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === 'finalized' && ($this->configService->context()['has_legacy'] ?? false)) {
|
||||
if ($status === 'finalized' && ($context['has_legacy'] ?? false)) {
|
||||
$update['is_legacy'] = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,49 @@ class ExamDraftConfigService
|
||||
|
||||
public function context(): array
|
||||
{
|
||||
$teacherUserColumn = $this->hasColumn('exam_drafts', 'teacher_id')
|
||||
? 'teacher_id'
|
||||
: ($this->hasColumn('exam_drafts', 'author_id') ? 'author_id' : null);
|
||||
|
||||
$teacherFileColumn = $this->hasColumn('exam_drafts', 'teacher_file')
|
||||
? 'teacher_file'
|
||||
: ($this->hasColumn('exam_drafts', 'author_file') ? 'author_file' : null);
|
||||
|
||||
$teacherFilenameColumn = $this->hasColumn('exam_drafts', 'teacher_filename')
|
||||
? 'teacher_filename'
|
||||
: ($this->hasColumn('exam_drafts', 'author_filename') ? 'author_filename' : null);
|
||||
|
||||
$teacherCommentColumn = $this->hasColumn('exam_drafts', 'description')
|
||||
? 'description'
|
||||
: ($this->hasColumn('exam_drafts', 'author_comment') ? 'author_comment' : null);
|
||||
|
||||
$adminUserColumn = $this->hasColumn('exam_drafts', 'admin_id')
|
||||
? 'admin_id'
|
||||
: ($this->hasColumn('exam_drafts', 'reviewer_id') ? 'reviewer_id' : null);
|
||||
|
||||
$adminCommentsColumn = $this->hasColumn('exam_drafts', 'admin_comments')
|
||||
? 'admin_comments'
|
||||
: ($this->hasColumn('exam_drafts', 'reviewer_comments') ? 'reviewer_comments' : null);
|
||||
|
||||
return [
|
||||
'school_year' => (string) (Configuration::getConfig('school_year') ?? ''),
|
||||
'semester' => (string) (Configuration::getConfig('semester') ?? ''),
|
||||
'teacher_user_column' => $teacherUserColumn,
|
||||
'teacher_file_column' => $teacherFileColumn,
|
||||
'teacher_filename_column' => $teacherFilenameColumn,
|
||||
'teacher_comment_column' => $teacherCommentColumn,
|
||||
'admin_user_column' => $adminUserColumn,
|
||||
'admin_comments_column' => $adminCommentsColumn,
|
||||
'has_teacher_id' => $this->hasColumn('exam_drafts', 'teacher_id'),
|
||||
'has_admin_id' => $this->hasColumn('exam_drafts', 'admin_id'),
|
||||
'has_author_id' => $this->hasColumn('exam_drafts', 'author_id'),
|
||||
'has_reviewer_id' => $this->hasColumn('exam_drafts', 'reviewer_id'),
|
||||
'has_admin_comments' => $this->hasColumn('exam_drafts', 'admin_comments'),
|
||||
'has_reviewer_comments' => $this->hasColumn('exam_drafts', 'reviewer_comments'),
|
||||
'has_reviewed_at' => $this->hasColumn('exam_drafts', 'reviewed_at'),
|
||||
'has_final_pdf' => $this->hasColumn('exam_drafts', 'final_pdf_file'),
|
||||
'has_final_filename' => $this->hasColumn('exam_drafts', 'final_filename'),
|
||||
'has_previous_draft_id' => $this->hasColumn('exam_drafts', 'previous_draft_id'),
|
||||
'has_legacy' => $this->hasColumn('exam_drafts', 'is_legacy'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -18,15 +18,19 @@ class ExamDraftTeacherService
|
||||
{
|
||||
$context = $this->configService->context();
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$teacherUserColumn = (string) ($context['teacher_user_column'] ?? '');
|
||||
|
||||
$assignments = TeacherClass::getClassAssignmentsByUserId($teacherId, $schoolYear);
|
||||
$classSectionIds = array_map(static fn ($a) => (int) $a['class_section_id'], $assignments);
|
||||
|
||||
$drafts = [];
|
||||
if ($teacherUserColumn !== '') {
|
||||
$drafts = ExamDraft::query()
|
||||
->where('teacher_id', $teacherId)
|
||||
->where($teacherUserColumn, $teacherId)
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$legacyExams = [];
|
||||
if (($context['has_legacy'] ?? false) && !empty($classSectionIds)) {
|
||||
@@ -94,11 +98,14 @@ class ExamDraftTeacherService
|
||||
}
|
||||
|
||||
$existingDraft = ExamDraft::query()
|
||||
->where('teacher_id', $teacherId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('version')
|
||||
->when(
|
||||
(string) ($context['teacher_user_column'] ?? '') !== '',
|
||||
fn ($query) => $query->where((string) $context['teacher_user_column'], $teacherId)
|
||||
)
|
||||
->first();
|
||||
|
||||
$nextVersion = 1;
|
||||
@@ -110,20 +117,45 @@ class ExamDraftTeacherService
|
||||
|
||||
$title = $examType !== '' ? $examType : 'Exam Draft';
|
||||
|
||||
$draft = ExamDraft::query()->create([
|
||||
'teacher_id' => $teacherId,
|
||||
$createPayload = [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'exam_type' => $examType !== '' ? $examType : null,
|
||||
'draft_title' => $title,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'teacher_file' => $teacherFile,
|
||||
'teacher_filename' => $teacherFilename,
|
||||
'status' => 'submitted',
|
||||
'version' => $nextVersion,
|
||||
'previous_draft_id' => $previousId,
|
||||
]);
|
||||
];
|
||||
|
||||
if (($context['teacher_user_column'] ?? null) === 'teacher_id') {
|
||||
$createPayload['teacher_id'] = $teacherId;
|
||||
}
|
||||
if (($context['teacher_user_column'] ?? null) === 'author_id') {
|
||||
$createPayload['author_id'] = $teacherId;
|
||||
}
|
||||
if (($context['teacher_comment_column'] ?? null) === 'description') {
|
||||
$createPayload['description'] = $description !== '' ? $description : null;
|
||||
}
|
||||
if (($context['teacher_comment_column'] ?? null) === 'author_comment') {
|
||||
$createPayload['author_comment'] = $description !== '' ? $description : null;
|
||||
}
|
||||
if (($context['teacher_file_column'] ?? null) === 'teacher_file') {
|
||||
$createPayload['teacher_file'] = $teacherFile;
|
||||
}
|
||||
if (($context['teacher_file_column'] ?? null) === 'author_file') {
|
||||
$createPayload['author_file'] = $teacherFile;
|
||||
}
|
||||
if (($context['teacher_filename_column'] ?? null) === 'teacher_filename') {
|
||||
$createPayload['teacher_filename'] = $teacherFilename;
|
||||
}
|
||||
if (($context['teacher_filename_column'] ?? null) === 'author_filename') {
|
||||
$createPayload['author_filename'] = $teacherFilename;
|
||||
}
|
||||
if ($context['has_previous_draft_id'] ?? false) {
|
||||
$createPayload['previous_draft_id'] = $previousId;
|
||||
}
|
||||
|
||||
$draft = ExamDraft::query()->create($createPayload);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
namespace App\Services\Grading;
|
||||
|
||||
use App\Models\BelowSixtyDecision;
|
||||
use App\Models\CurrentFlag;
|
||||
use App\Models\ParentMeetingSchedule;
|
||||
use App\Models\ScoreComment;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentDecision;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use RuntimeException;
|
||||
@@ -258,6 +260,330 @@ class GradingBelowSixtyService
|
||||
]);
|
||||
}
|
||||
|
||||
public function listDecisionRows(string $schoolYear): array
|
||||
{
|
||||
$scoreRows = DB::table('semester_scores as ss')
|
||||
->select([
|
||||
's.id as student_id',
|
||||
's.school_id',
|
||||
's.firstname',
|
||||
's.lastname',
|
||||
's.age',
|
||||
'ss.class_section_id',
|
||||
'cs.class_section_name',
|
||||
DB::raw('LOWER(TRIM(ss.semester)) as sem_key'),
|
||||
'ss.semester_score',
|
||||
])
|
||||
->join('students as s', 's.id', '=', 'ss.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
|
||||
->where('s.is_active', 1)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(ss.semester)) IN (?, ?)', ['fall', 'spring'])
|
||||
->whereNotNull('ss.semester_score')
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderBy('s.lastname', 'ASC')
|
||||
->orderBy('s.firstname', 'ASC')
|
||||
->orderByDesc('ss.updated_at')
|
||||
->orderByDesc('ss.id')
|
||||
->get();
|
||||
|
||||
if ($scoreRows->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$studentMap = [];
|
||||
foreach ($scoreRows as $row) {
|
||||
$studentId = (int) ($row->student_id ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($studentMap[$studentId])) {
|
||||
$studentMap[$studentId] = [
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $row->school_id,
|
||||
'firstname' => $row->firstname,
|
||||
'lastname' => $row->lastname,
|
||||
'age' => $row->age,
|
||||
'class_section_id' => (int) ($row->class_section_id ?? 0),
|
||||
'class_section_name' => $row->class_section_name,
|
||||
'fall_score' => null,
|
||||
'spring_score' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$semesterKey = strtolower(trim((string) ($row->sem_key ?? '')));
|
||||
$score = is_numeric($row->semester_score ?? null) ? (float) $row->semester_score : null;
|
||||
if ($score === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($semesterKey === 'fall' && $studentMap[$studentId]['fall_score'] === null) {
|
||||
$studentMap[$studentId]['fall_score'] = $score;
|
||||
}
|
||||
|
||||
if ($semesterKey === 'spring' && $studentMap[$studentId]['spring_score'] === null) {
|
||||
$studentMap[$studentId]['spring_score'] = $score;
|
||||
}
|
||||
}
|
||||
|
||||
$studentIds = array_keys($studentMap);
|
||||
if (empty($studentIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$belowDecisionMap = BelowSixtyDecision::query()
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', 'year')
|
||||
->whereIn('student_id', $studentIds)
|
||||
->get()
|
||||
->keyBy(static fn (BelowSixtyDecision $row) => (int) $row->student_id);
|
||||
|
||||
$studentDecisionMap = StudentDecision::query()
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('student_id', $studentIds)
|
||||
->get()
|
||||
->keyBy(static fn (StudentDecision $row) => (int) $row->student_id);
|
||||
|
||||
$certificateRows = DB::table('certificate_records')
|
||||
->select('student_id', 'certificate_number', 'issued_at')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('student_id', $studentIds)
|
||||
->orderByDesc('issued_at')
|
||||
->get();
|
||||
|
||||
$certificateMap = [];
|
||||
foreach ($certificateRows as $row) {
|
||||
$studentId = (int) ($row->student_id ?? 0);
|
||||
if ($studentId > 0 && !isset($certificateMap[$studentId])) {
|
||||
$certificateMap[$studentId] = (string) ($row->certificate_number ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($studentMap as $studentId => $info) {
|
||||
$fall = $info['fall_score'];
|
||||
$spring = $info['spring_score'];
|
||||
|
||||
if ($fall === null || $spring === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$yearScore = round(($fall + $spring) / 2, 2);
|
||||
if ($yearScore >= 60) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$belowDecision = $belowDecisionMap->get($studentId);
|
||||
$studentDecision = $studentDecisionMap->get($studentId);
|
||||
|
||||
if ($studentDecision !== null && is_numeric($studentDecision->year_score)) {
|
||||
$yearScore = round((float) $studentDecision->year_score, 2);
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'student_id' => $studentId,
|
||||
'school_id' => $info['school_id'],
|
||||
'firstname' => $info['firstname'],
|
||||
'lastname' => $info['lastname'],
|
||||
'age' => $info['age'],
|
||||
'class_section_id' => $info['class_section_id'],
|
||||
'class_section_name' => $info['class_section_name'],
|
||||
'fall_score' => $fall,
|
||||
'spring_score' => $spring,
|
||||
'year_score' => $yearScore,
|
||||
'decision' => (string) ($belowDecision?->decision ?? ''),
|
||||
'decision_notes' => (string) ($belowDecision?->notes ?? ''),
|
||||
'consolidated_decision' => $studentDecision?->decision,
|
||||
'certificate_number' => $certificateMap[$studentId] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function saveDecision(int $studentId, string $schoolYear, string $decision, string $notes, ?int $userId): array
|
||||
{
|
||||
if ($studentId <= 0 || trim($schoolYear) === '') {
|
||||
throw new RuntimeException('Missing student or school year.');
|
||||
}
|
||||
|
||||
$decision = trim($decision);
|
||||
$notes = trim($notes);
|
||||
|
||||
$belowDecision = BelowSixtyDecision::query()->firstOrNew([
|
||||
'student_id' => $studentId,
|
||||
'semester' => 'year',
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
$belowDecision->decision = $decision !== '' ? $decision : null;
|
||||
$belowDecision->notes = $notes !== '' ? $notes : null;
|
||||
$belowDecision->decided_by = $userId;
|
||||
$belowDecision->save();
|
||||
|
||||
$scoreRows = DB::table('semester_scores as ss')
|
||||
->select([
|
||||
DB::raw('LOWER(TRIM(ss.semester)) as sem_key'),
|
||||
'ss.semester_score',
|
||||
'cs.class_section_name',
|
||||
])
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
|
||||
->where('ss.student_id', $studentId)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(ss.semester)) IN (?, ?)', ['fall', 'spring'])
|
||||
->whereNotNull('ss.semester_score')
|
||||
->orderByDesc('ss.updated_at')
|
||||
->orderByDesc('ss.id')
|
||||
->get();
|
||||
|
||||
$fallScore = null;
|
||||
$springScore = null;
|
||||
$classSectionName = null;
|
||||
foreach ($scoreRows as $row) {
|
||||
$semesterKey = strtolower(trim((string) ($row->sem_key ?? '')));
|
||||
$score = is_numeric($row->semester_score ?? null) ? (float) $row->semester_score : null;
|
||||
if ($classSectionName === null && trim((string) ($row->class_section_name ?? '')) !== '') {
|
||||
$classSectionName = (string) $row->class_section_name;
|
||||
}
|
||||
if ($score === null) {
|
||||
continue;
|
||||
}
|
||||
if ($semesterKey === 'fall' && $fallScore === null) {
|
||||
$fallScore = $score;
|
||||
}
|
||||
if ($semesterKey === 'spring' && $springScore === null) {
|
||||
$springScore = $score;
|
||||
}
|
||||
}
|
||||
|
||||
if ($classSectionName === null) {
|
||||
$enrollment = DB::table('student_class as sc')
|
||||
->select('cs.class_section_name')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('sc.student_id', $studentId)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->orderByDesc('sc.id')
|
||||
->first();
|
||||
$classSectionName = $enrollment?->class_section_name;
|
||||
}
|
||||
|
||||
$yearScore = null;
|
||||
if ($fallScore !== null && $springScore !== null) {
|
||||
$yearScore = round(($fallScore + $springScore) / 2, 2);
|
||||
} elseif ($fallScore !== null) {
|
||||
$yearScore = round($fallScore, 2);
|
||||
} elseif ($springScore !== null) {
|
||||
$yearScore = round($springScore, 2);
|
||||
}
|
||||
|
||||
$studentDecision = StudentDecision::query()->firstOrNew([
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
$studentDecision->class_section_name = $classSectionName;
|
||||
$studentDecision->year_score = $yearScore;
|
||||
$studentDecision->decision = $decision !== '' ? $decision : null;
|
||||
$studentDecision->source = $decision !== '' ? 'manual' : 'pending';
|
||||
$studentDecision->notes = $notes !== '' ? $notes : null;
|
||||
$studentDecision->generated_by = $userId;
|
||||
$studentDecision->save();
|
||||
|
||||
return [
|
||||
'student_id' => $studentId,
|
||||
'school_year' => $schoolYear,
|
||||
'decision' => $decision,
|
||||
'notes' => $notes,
|
||||
'year_score' => $yearScore,
|
||||
'class_section_name' => $classSectionName,
|
||||
];
|
||||
}
|
||||
|
||||
public function studentDecisionDetails(int $studentId, string $schoolYear): array
|
||||
{
|
||||
if ($studentId <= 0 || trim($schoolYear) === '') {
|
||||
throw new RuntimeException('Missing student or school year.');
|
||||
}
|
||||
|
||||
return [
|
||||
'semesters' => $this->fetchAllSemestersForStudent($studentId, $schoolYear),
|
||||
];
|
||||
}
|
||||
|
||||
public function decisionEmailContext(int $studentId, string $schoolYear): array
|
||||
{
|
||||
$context = $this->getBelowSixtyDecisionEmailContext($studentId, $schoolYear);
|
||||
if (empty($context['student'])) {
|
||||
throw new RuntimeException('Student not found.');
|
||||
}
|
||||
|
||||
$decision = trim((string) ($context['decision_row']['decision'] ?? ''));
|
||||
if ($decision === '') {
|
||||
throw new RuntimeException('No saved decision found for this student. Save a decision first.');
|
||||
}
|
||||
|
||||
$subject = 'Whole Year Academic Decision — '
|
||||
. (string) ($context['student_name'] ?? 'Student')
|
||||
. ' (' . $schoolYear . ')';
|
||||
|
||||
return [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => (string) ($context['student_name'] ?? 'Student'),
|
||||
'class_section_name' => (string) ($context['class_section_name'] ?? ''),
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => 'year',
|
||||
'decision' => $decision,
|
||||
'subject' => $subject,
|
||||
'html' => $this->buildDecisionEmailHtml($context),
|
||||
'year_score' => $context['year_score'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
public function sendDecisionEmail(int $studentId, string $schoolYear, ?string $subject, ?string $html): void
|
||||
{
|
||||
$context = $this->getBelowSixtyDecisionEmailContext($studentId, $schoolYear);
|
||||
if (empty($context['student'])) {
|
||||
throw new RuntimeException('Student not found.');
|
||||
}
|
||||
|
||||
$decision = trim((string) ($context['decision_row']['decision'] ?? ''));
|
||||
if ($decision === '') {
|
||||
throw new RuntimeException('No saved decision found for this student.');
|
||||
}
|
||||
|
||||
$resolvedSubject = trim((string) $subject);
|
||||
if ($resolvedSubject === '') {
|
||||
$resolvedSubject = 'Whole Year Academic Decision — '
|
||||
. (string) ($context['student_name'] ?? 'Student')
|
||||
. ' (' . $schoolYear . ')';
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'student_id' => $studentId,
|
||||
'student_name' => (string) ($context['student_name'] ?? 'Student'),
|
||||
'class_section_name' => (string) ($context['class_section_name'] ?? ''),
|
||||
'semester' => 'year',
|
||||
'school_year' => $schoolYear,
|
||||
'decision' => $decision,
|
||||
'notes' => (string) ($context['decision_row']['notes'] ?? ''),
|
||||
'subject' => $resolvedSubject,
|
||||
'scores' => [
|
||||
'fall_score' => $context['fall_score'] ?? null,
|
||||
'spring_score' => $context['spring_score'] ?? null,
|
||||
'year_score' => $context['year_score'] ?? null,
|
||||
],
|
||||
'all_semesters' => $context['all_semesters'] ?? [],
|
||||
];
|
||||
|
||||
$resolvedHtml = trim((string) $html);
|
||||
if ($resolvedHtml !== '') {
|
||||
$payload['html'] = $resolvedHtml;
|
||||
} else {
|
||||
$payload['html'] = $this->buildDecisionEmailHtml($context);
|
||||
}
|
||||
|
||||
$this->sendEmail($payload);
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyEmailRow(int $studentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
$semesterKey = strtolower(trim($semester));
|
||||
@@ -303,6 +629,71 @@ class GradingBelowSixtyService
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function fetchAllSemestersForStudent(int $studentId, string $schoolYear): array
|
||||
{
|
||||
$rows = DB::table('semester_scores as ss')
|
||||
->select([
|
||||
'ss.semester',
|
||||
'cs.class_section_name',
|
||||
'ss.homework_avg',
|
||||
'ss.project_avg',
|
||||
'ss.participation_score',
|
||||
DB::raw('COALESCE(ss.test_avg, ss.quiz_avg) as test_avg'),
|
||||
'ss.ptap_score',
|
||||
'ss.attendance_score',
|
||||
'ss.midterm_exam_score',
|
||||
'ss.final_exam_score',
|
||||
'ss.semester_score',
|
||||
])
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
|
||||
->where('ss.student_id', $studentId)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->orderBy('ss.semester')
|
||||
->get();
|
||||
|
||||
$semesters = [];
|
||||
foreach ($rows as $row) {
|
||||
$semester = ucfirst(strtolower(trim((string) ($row->semester ?? ''))));
|
||||
$semesters[$semester] = [
|
||||
'semester' => $semester,
|
||||
'class_section_name' => $row->class_section_name,
|
||||
'homework_avg' => $row->homework_avg,
|
||||
'project_avg' => $row->project_avg,
|
||||
'participation_score' => $row->participation_score,
|
||||
'test_avg' => $row->test_avg,
|
||||
'ptap_score' => $row->ptap_score,
|
||||
'attendance_score' => $row->attendance_score,
|
||||
'midterm_exam_score' => $row->midterm_exam_score,
|
||||
'final_exam_score' => $row->final_exam_score,
|
||||
'semester_score' => $row->semester_score,
|
||||
'comments' => [],
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($semesters)) {
|
||||
$commentRows = DB::table('score_comments')
|
||||
->select('semester', 'score_type', 'comment', 'created_at')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereNotNull('comment')
|
||||
->where('comment', '!=', '')
|
||||
->orderBy('semester')
|
||||
->orderBy('score_type')
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
foreach ($commentRows as $row) {
|
||||
$semester = ucfirst(strtolower(trim((string) ($row->semester ?? ''))));
|
||||
$type = strtolower(trim((string) ($row->score_type ?? 'general')));
|
||||
if (isset($semesters[$semester]) && !isset($semesters[$semester]['comments'][$type])) {
|
||||
$semesters[$semester]['comments'][$type] = (string) ($row->comment ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($semesters);
|
||||
}
|
||||
|
||||
private function fetchBelowSixtyParentName(int $studentId): string
|
||||
{
|
||||
$parentName = 'Parent/Guardian';
|
||||
@@ -328,6 +719,274 @@ class GradingBelowSixtyService
|
||||
return $parentName;
|
||||
}
|
||||
|
||||
private function getBelowSixtyDecisionEmailContext(int $studentId, string $schoolYear): array
|
||||
{
|
||||
$student = DB::table('students as s')
|
||||
->select(['s.id', 's.firstname', 's.lastname', 's.is_active'])
|
||||
->where('s.id', $studentId)
|
||||
->first();
|
||||
|
||||
if ($student === null) {
|
||||
return [
|
||||
'student' => null,
|
||||
'decision_row' => null,
|
||||
'student_name' => '',
|
||||
'class_section_name' => '',
|
||||
'fall_score' => null,
|
||||
'spring_score' => null,
|
||||
'year_score' => null,
|
||||
'all_semesters' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$studentName = trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? ''));
|
||||
if ($studentName === '') {
|
||||
$studentName = 'Student';
|
||||
}
|
||||
|
||||
$decisionRow = BelowSixtyDecision::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', 'year')
|
||||
->first();
|
||||
|
||||
$scoreRows = DB::table('semester_scores as ss')
|
||||
->select([
|
||||
DB::raw('LOWER(TRIM(ss.semester)) as sem_key'),
|
||||
'ss.semester',
|
||||
'ss.semester_score',
|
||||
'ss.class_section_id',
|
||||
'cs.class_section_name',
|
||||
])
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ss.class_section_id')
|
||||
->where('ss.student_id', $studentId)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->whereRaw('LOWER(TRIM(ss.semester)) IN (?, ?)', ['fall', 'spring'])
|
||||
->whereNotNull('ss.semester_score')
|
||||
->orderByDesc('ss.updated_at')
|
||||
->orderByDesc('ss.id')
|
||||
->get();
|
||||
|
||||
$fallScore = null;
|
||||
$springScore = null;
|
||||
$classSectionName = '';
|
||||
foreach ($scoreRows as $row) {
|
||||
$semesterKey = strtolower(trim((string) ($row->sem_key ?? '')));
|
||||
$score = is_numeric($row->semester_score ?? null) ? (float) $row->semester_score : null;
|
||||
if ($classSectionName === '' && trim((string) ($row->class_section_name ?? '')) !== '') {
|
||||
$classSectionName = (string) $row->class_section_name;
|
||||
}
|
||||
if ($score === null) {
|
||||
continue;
|
||||
}
|
||||
if ($semesterKey === 'fall' && $fallScore === null) {
|
||||
$fallScore = $score;
|
||||
}
|
||||
if ($semesterKey === 'spring' && $springScore === null) {
|
||||
$springScore = $score;
|
||||
}
|
||||
}
|
||||
|
||||
if ($classSectionName === '') {
|
||||
$enrollment = DB::table('student_class as sc')
|
||||
->select('cs.class_section_name')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('sc.student_id', $studentId)
|
||||
->where('sc.school_year', $schoolYear)
|
||||
->orderByDesc('sc.id')
|
||||
->first();
|
||||
$classSectionName = (string) ($enrollment?->class_section_name ?? '');
|
||||
}
|
||||
|
||||
$yearScore = null;
|
||||
if ($fallScore !== null && $springScore !== null) {
|
||||
$yearScore = round(($fallScore + $springScore) / 2, 2);
|
||||
} elseif ($fallScore !== null) {
|
||||
$yearScore = round($fallScore, 2);
|
||||
} elseif ($springScore !== null) {
|
||||
$yearScore = round($springScore, 2);
|
||||
}
|
||||
|
||||
return [
|
||||
'student' => (array) $student,
|
||||
'decision_row' => $decisionRow?->toArray(),
|
||||
'student_name' => $studentName,
|
||||
'class_section_name' => $classSectionName,
|
||||
'school_year' => $schoolYear,
|
||||
'fall_score' => $fallScore,
|
||||
'spring_score' => $springScore,
|
||||
'year_score' => $yearScore,
|
||||
'all_semesters' => $this->fetchAllSemestersForStudent($studentId, $schoolYear),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildDecisionEmailHtml(array $context): string
|
||||
{
|
||||
$studentName = (string) ($context['student_name'] ?? 'Student');
|
||||
$schoolYear = (string) ($context['school_year'] ?? '');
|
||||
$classSectionName = (string) ($context['class_section_name'] ?? '');
|
||||
$decision = trim((string) ($context['decision_row']['decision'] ?? ''));
|
||||
$notes = trim((string) ($context['decision_row']['notes'] ?? ''));
|
||||
$fallScore = $context['fall_score'] ?? null;
|
||||
$springScore = $context['spring_score'] ?? null;
|
||||
$yearScore = $context['year_score'] ?? null;
|
||||
|
||||
$fallText = $fallScore !== null ? number_format((float) $fallScore, 2) : 'N/A';
|
||||
$springText = $springScore !== null ? number_format((float) $springScore, 2) : 'N/A';
|
||||
$yearText = $yearScore !== null ? number_format((float) $yearScore, 2) : 'N/A';
|
||||
|
||||
$html = '
|
||||
<p>Dear Parent/Guardian,</p>
|
||||
<p>
|
||||
This message is regarding <strong>' . e($studentName) . '</strong>
|
||||
for the <strong>' . e($schoolYear) . '</strong> school year.
|
||||
</p>
|
||||
<p>
|
||||
Class: <strong>' . e($classSectionName !== '' ? $classSectionName : 'N/A') . '</strong><br>
|
||||
Fall Score: <strong>' . e($fallText) . '</strong><br>
|
||||
Spring Score: <strong>' . e($springText) . '</strong><br>
|
||||
Whole-Year Score: <strong>' . e($yearText) . '</strong><br>
|
||||
Decision: <strong>' . e($decision) . '</strong>
|
||||
</p>';
|
||||
|
||||
if ($notes !== '') {
|
||||
$html .= '
|
||||
<p>
|
||||
<strong>Decision Notes:</strong><br>
|
||||
' . nl2br(e($notes)) . '
|
||||
</p>';
|
||||
}
|
||||
|
||||
$html .= $this->buildDecisionEmailDetailsHtml($context['all_semesters'] ?? []);
|
||||
|
||||
$html .= '
|
||||
<p>Please contact the school administration if you have any questions.</p>
|
||||
<p>Regards,<br>Al Rahma Sunday School</p>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildDecisionEmailDetailsHtml(array $semesters): string
|
||||
{
|
||||
if (empty($semesters)) {
|
||||
return '
|
||||
<p><strong>Detailed Scores and Comments</strong></p>
|
||||
<p>No detailed score data found.</p>';
|
||||
}
|
||||
|
||||
$scoreLabels = [
|
||||
'homework_avg' => 'Homework Avg',
|
||||
'project_avg' => 'Project Avg',
|
||||
'participation_score' => 'Participation',
|
||||
'test_avg' => 'Test Avg',
|
||||
'ptap_score' => 'PTAP Score',
|
||||
'attendance_score' => 'Attendance',
|
||||
'midterm_exam_score' => 'Midterm Score',
|
||||
'final_exam_score' => 'Final Exam',
|
||||
'semester_score' => 'Semester Score',
|
||||
];
|
||||
|
||||
$commentTypeLabels = [
|
||||
'general' => 'General',
|
||||
'attendance' => 'Attendance',
|
||||
'attendance_comment' => 'Attendance',
|
||||
'midterm' => 'Midterm',
|
||||
'final' => 'Final Exam',
|
||||
'ptap' => 'PTAP',
|
||||
];
|
||||
|
||||
$html = '
|
||||
<hr>
|
||||
<p><strong>Detailed Scores and Comments</strong></p>';
|
||||
|
||||
foreach ($semesters as $semester) {
|
||||
$semesterName = trim((string) ($semester['semester'] ?? ''));
|
||||
if ($semesterName === '') {
|
||||
$semesterName = 'Semester';
|
||||
}
|
||||
|
||||
$classSectionName = trim((string) ($semester['class_section_name'] ?? ''));
|
||||
$html .= '
|
||||
<p style="margin-top:16px;margin-bottom:6px;">
|
||||
<strong>' . e($semesterName) . ' Semester</strong>';
|
||||
if ($classSectionName !== '') {
|
||||
$html .= ' — ' . e($classSectionName);
|
||||
}
|
||||
$html .= '
|
||||
</p>
|
||||
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%;margin-bottom:10px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" style="background:#f2f2f2;">Item</th>
|
||||
<th align="right" style="background:#f2f2f2;">Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
$hasScoreRow = false;
|
||||
foreach ($scoreLabels as $key => $label) {
|
||||
if (!array_key_exists($key, $semester)) {
|
||||
continue;
|
||||
}
|
||||
$value = $semester[$key];
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
$scoreText = is_numeric($value) ? number_format((float) $value, 2) : (string) $value;
|
||||
$fontWeight = $key === 'semester_score' ? 'font-weight:bold;' : '';
|
||||
$html .= '
|
||||
<tr>
|
||||
<td>' . e($label) . '</td>
|
||||
<td align="right" style="' . $fontWeight . '">' . e($scoreText) . '</td>
|
||||
</tr>';
|
||||
$hasScoreRow = true;
|
||||
}
|
||||
|
||||
if (!$hasScoreRow) {
|
||||
$html .= '
|
||||
<tr>
|
||||
<td colspan="2">No scores recorded.</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$html .= '
|
||||
</tbody>
|
||||
</table>';
|
||||
|
||||
$comments = is_array($semester['comments'] ?? null) ? $semester['comments'] : [];
|
||||
if (!empty($comments)) {
|
||||
$deduped = [];
|
||||
$seen = [];
|
||||
foreach ($comments as $type => $text) {
|
||||
$text = trim((string) $text);
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
$label = $commentTypeLabels[$type] ?? (string) $type;
|
||||
$hash = $label . '|' . $text;
|
||||
if (isset($seen[$hash])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$hash] = true;
|
||||
$deduped[] = ['label' => $label, 'text' => $text];
|
||||
}
|
||||
|
||||
if (!empty($deduped)) {
|
||||
$html .= '<p style="margin:8px 0 4px;"><strong>Comments</strong></p>';
|
||||
foreach ($deduped as $comment) {
|
||||
$html .= '
|
||||
<p style="margin:4px 0 8px;padding:8px;background:#f8f9fa;border-left:4px solid #999;">
|
||||
<strong>' . e($comment['label']) . ':</strong><br>
|
||||
' . nl2br(e($comment['text'])) . '
|
||||
</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBelowSixtySubject(string $studentName, string $semester, string $schoolYear): string
|
||||
{
|
||||
$subject = 'Student Performance Alert';
|
||||
|
||||
+11
-4
@@ -2,6 +2,13 @@
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
$mysqlSslCaOption = null;
|
||||
if (extension_loaded('pdo_mysql')) {
|
||||
$mysqlSslCaOption = defined('Pdo\\Mysql::ATTR_SSL_CA')
|
||||
? constant('Pdo\\Mysql::ATTR_SSL_CA')
|
||||
: (defined('PDO::MYSQL_ATTR_SSL_CA') ? constant('PDO::MYSQL_ATTR_SSL_CA') : null);
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
@@ -58,8 +65,8 @@ return [
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
'options' => $mysqlSslCaOption !== null ? array_filter([
|
||||
$mysqlSslCaOption => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
@@ -78,8 +85,8 @@ return [
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
'options' => $mysqlSslCaOption !== null ? array_filter([
|
||||
$mysqlSslCaOption => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
||||
@@ -11,30 +11,9 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
// This project has authoritative legacy-schema migrations for users,
|
||||
// password reset tables, and sessions. Keep the starter migration inert
|
||||
// so migrate:fresh follows the provided school_api schema instead.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,8 +21,6 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
// No-op: the dump-aligned migrations own these tables.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,17 +11,8 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
// The dump-aligned cache migration runs later with the exact schema and
|
||||
// indexes from school_api.sql. Leave this starter migration inert.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +20,6 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
// No-op: the dump-aligned migration owns these tables.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,38 +11,8 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
// school_api.sql does not define Laravel queue tables. Keep this starter
|
||||
// migration inert so fresh migrations stay aligned with the provided dump.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,8 +20,6 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
// No-op: this migration does not create any tables.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('certificate_records')) {
|
||||
return;
|
||||
}
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
CREATE TABLE `certificate_records` (
|
||||
`id` int UNSIGNED NOT NULL,
|
||||
`certificate_number` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`student_id` int UNSIGNED NOT NULL,
|
||||
`student_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`grade` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`cert_date` date DEFAULT NULL,
|
||||
`school_year` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`class_section_id` int UNSIGNED DEFAULT NULL,
|
||||
`issued_by` int UNSIGNED DEFAULT NULL,
|
||||
`issued_at` datetime DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT NULL,
|
||||
`updated_at` datetime DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `certificate_records`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD UNIQUE KEY `certificate_number` (`certificate_number`),
|
||||
ADD KEY `idx_school_year_student` (`school_year`,`student_id`);
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `certificate_records`
|
||||
MODIFY `id` int UNSIGNED NOT NULL AUTO_INCREMENT;
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('certificate_records');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('flag')) {
|
||||
return;
|
||||
}
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
CREATE TABLE `flag` (
|
||||
`id` int UNSIGNED NOT NULL,
|
||||
`student_id` int NOT NULL,
|
||||
`student_name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`grade` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`incident` enum('payment','attendance','grade','behavior','talking_off_task','calling_out','minor_disruption','minor_dress_code','forgetting_materials','repeated_defiance','disrespectful_tone','minor_profanity','repeated_tardiness','device_misuse','minor_conflict','bullying_harassment','fighting_aggression','threats','theft','vandalism','sexual_harassment','serious_cyber_incident','contraband','weapons_drugs','other') COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`incident_datetime` datetime NOT NULL,
|
||||
`incident_state` enum('Open','Closed','Canceled') COLLATE utf8mb4_general_ci DEFAULT 'Open',
|
||||
`updated_by_open` int DEFAULT NULL,
|
||||
`open_description` text COLLATE utf8mb4_general_ci,
|
||||
`updated_by_closed` int DEFAULT NULL,
|
||||
`close_description` text COLLATE utf8mb4_general_ci,
|
||||
`action_taken` text COLLATE utf8mb4_general_ci,
|
||||
`updated_by_canceled` int DEFAULT NULL,
|
||||
`cancel_description` text COLLATE utf8mb4_general_ci,
|
||||
`semester` varchar(25) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
||||
`school_year` varchar(9) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `flag`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `flag`
|
||||
MODIFY `id` int UNSIGNED NOT NULL AUTO_INCREMENT;
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('flag');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('below_sixty_decisions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
CREATE TABLE `below_sixty_decisions` (
|
||||
`id` int unsigned NOT NULL,
|
||||
`student_id` int unsigned NOT NULL,
|
||||
`semester` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`school_year` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`decision` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`notes` text COLLATE utf8mb4_general_ci,
|
||||
`decided_by` int unsigned DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT NULL,
|
||||
`updated_at` datetime DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `below_sixty_decisions`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD UNIQUE KEY `student_id_semester_school_year` (`student_id`,`semester`,`school_year`),
|
||||
ADD KEY `school_year_semester` (`school_year`,`semester`);
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `below_sixty_decisions`
|
||||
MODIFY `id` int unsigned NOT NULL AUTO_INCREMENT;
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('below_sixty_decisions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('student_decisions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
CREATE TABLE `student_decisions` (
|
||||
`id` int unsigned NOT NULL,
|
||||
`student_id` int unsigned NOT NULL,
|
||||
`school_year` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`class_section_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`year_score` decimal(6,2) DEFAULT NULL,
|
||||
`decision` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`source` enum('auto','manual','pending') COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'auto',
|
||||
`notes` text COLLATE utf8mb4_general_ci,
|
||||
`generated_by` int unsigned DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT NULL,
|
||||
`updated_at` datetime DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `student_decisions`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD UNIQUE KEY `student_id_semester_school_year` (`student_id`,`school_year`),
|
||||
ADD KEY `school_year_semester` (`school_year`);
|
||||
SQL
|
||||
);
|
||||
|
||||
App\Support\SqliteCompat::statement(<<<'SQL'
|
||||
ALTER TABLE `student_decisions`
|
||||
MODIFY `id` int unsigned NOT NULL AUTO_INCREMENT;
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('student_decisions');
|
||||
}
|
||||
};
|
||||
+2
-1
@@ -19,11 +19,12 @@ LOG_DEPRECATIONS_CHANNEL=null
|
||||
# DATABASE
|
||||
# ----------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql_db
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=school_api
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
APP_URL=http://127.0.0.1:8000
|
||||
|
||||
# ----------------------------
|
||||
# SESSION
|
||||
@@ -1166,6 +1166,11 @@ Route::prefix('v1')->group(function () {
|
||||
Route::put('placement/batch/{batchId}', [GradingApiController::class, 'updatePlacementBatch']);
|
||||
|
||||
Route::get('below-sixty', [GradingApiController::class, 'belowSixty']);
|
||||
Route::get('below-sixty/decisions', [GradingApiController::class, 'belowSixtyDecisions']);
|
||||
Route::post('below-sixty/decisions', [GradingApiController::class, 'saveBelowSixtyDecision']);
|
||||
Route::get('below-sixty/decisions/details', [GradingApiController::class, 'belowSixtyDecisionDetails']);
|
||||
Route::get('below-sixty/decisions/email', [GradingApiController::class, 'belowSixtyDecisionEmail']);
|
||||
Route::post('below-sixty/decisions/email', [GradingApiController::class, 'sendBelowSixtyDecisionEmail']);
|
||||
Route::get('below-sixty/email', [GradingApiController::class, 'belowSixtyEmail']);
|
||||
Route::post('below-sixty/email', [GradingApiController::class, 'sendBelowSixtyEmail']);
|
||||
Route::post('below-sixty/status', [GradingApiController::class, 'updateBelowSixtyStatus']);
|
||||
|
||||
Reference in New Issue
Block a user