add more controller

This commit is contained in:
root
2026-03-10 00:48:32 -04:00
parent 5eeaec0257
commit 20ee70d153
151 changed files with 9481 additions and 8407 deletions
@@ -0,0 +1,225 @@
<?php
namespace App\Services\Exams;
use App\Models\ClassSection;
use App\Models\ExamDraft;
use Illuminate\Support\Facades\DB;
class ExamDraftAdminService
{
public function __construct(
private ExamDraftConfigService $configService,
private ExamDraftFileService $fileService
) {
}
public function list(): array
{
$context = $this->configService->context();
$drafts = 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',
'u.firstname as teacher_first',
'u.lastname as teacher_last',
'a.firstname as admin_first',
'a.lastname as admin_last'
)
->orderByDesc('ed.created_at')
->get()
->map(fn ($r) => (array) $r)
->all();
$classSections = ClassSection::query()
->select('class_section_id', 'class_section_name')
->orderBy('class_section_name')
->get()
->toArray();
$legacyByClass = [];
if ($context['has_legacy'] ?? false) {
foreach ($drafts as $row) {
$isLegacy = !empty($row['is_legacy']);
if (!$isLegacy) {
continue;
}
$cid = (int) ($row['class_section_id'] ?? 0);
if (!isset($legacyByClass[$cid])) {
$legacyByClass[$cid] = [
'class_section_id' => $cid,
'class_section_name' => $row['class_section_name'] ?? 'Class ' . $cid,
'items' => [],
];
}
$legacyByClass[$cid]['items'][] = $row;
}
}
return [
'drafts' => $drafts,
'classSections' => $classSections,
'legacyByClass' => $legacyByClass,
'examTypes' => ExamDraftConfigService::EXAM_TYPES,
'statusOptions' => ExamDraftConfigService::STATUS_OPTIONS,
'schoolYear' => (string) ($context['school_year'] ?? ''),
'semester' => (string) ($context['semester'] ?? ''),
'allowedExtensions' => ExamDraftConfigService::ADMIN_ALLOWED_EXTENSIONS,
'maxUploadBytes' => ExamDraftConfigService::MAX_UPLOAD_BYTES,
];
}
public function uploadLegacy(int $adminId, array $payload, \Illuminate\Http\UploadedFile $file): array
{
$context = $this->configService->context();
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
$schoolYear = trim((string) ($payload['school_year'] ?? $context['school_year'] ?? ''));
$semester = trim((string) ($payload['semester'] ?? $context['semester'] ?? ''));
$examType = trim((string) ($payload['exam_type'] ?? ''));
if ($classSectionId <= 0 || $schoolYear === '' || $semester === '') {
return ['ok' => false, 'message' => 'Select class section, school year, and semester.'];
}
$stored = $this->fileService->storeUploadedFile(
$file,
ExamDraftConfigService::FINAL_UPLOAD_DIR,
ExamDraftConfigService::ADMIN_ALLOWED_EXTENSIONS,
ExamDraftConfigService::MAX_UPLOAD_BYTES
);
if ($stored === null) {
return ['ok' => false, 'message' => 'File type not allowed or upload failed.'];
}
$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' => $stored['stored'],
'final_filename' => $stored['original'],
'status' => 'finalized',
'admin_id' => $adminId,
'reviewed_at' => now(),
'version' => 1,
];
if ($context['has_legacy'] ?? false) {
$payload['is_legacy'] = 1;
}
$pdfName = null;
if ($stored['extension'] === 'pdf') {
$pdfName = $stored['stored'];
} else {
$pdfName = $this->fileService->ensurePdfExists(
$stored['stored'],
$stored['extension'],
ExamDraftConfigService::FINAL_UPLOAD_DIR
);
}
if ($pdfName !== null && ($context['has_final_pdf'] ?? false)) {
$payload['final_pdf_file'] = $pdfName;
}
$draft = ExamDraft::query()->create($payload);
return ['ok' => true, 'draft' => $draft->toArray()];
}
public function review(int $adminId, array $payload, ?\Illuminate\Http\UploadedFile $file): array
{
$draftId = (int) ($payload['draft_id'] ?? 0);
if ($draftId <= 0) {
return ['ok' => false, 'message' => 'Invalid submission selected.'];
}
$draft = ExamDraft::query()->find($draftId);
if (!$draft) {
return ['ok' => false, 'message' => 'Submission not found.'];
}
$comments = trim((string) ($payload['admin_comments'] ?? ''));
$statusInput = trim((string) ($payload['review_status'] ?? ''));
$currentStatus = (string) ($draft->status ?? 'draft');
$status = $this->normalizeStatus($statusInput !== '' ? $statusInput : $currentStatus, $currentStatus);
if (!in_array($status, ExamDraftConfigService::STATUS_OPTIONS, true)) {
$status = 'reviewed';
}
$update = [
'status' => $status,
'admin_comments' => $comments !== '' ? $comments : null,
'admin_id' => $adminId,
'reviewed_at' => now(),
];
if ($file) {
$stored = $this->fileService->storeUploadedFile(
$file,
ExamDraftConfigService::FINAL_UPLOAD_DIR,
ExamDraftConfigService::ADMIN_ALLOWED_EXTENSIONS,
ExamDraftConfigService::MAX_UPLOAD_BYTES
);
if ($stored === null) {
return ['ok' => false, 'message' => 'Unable to store the final draft.'];
}
$update['final_file'] = $stored['stored'];
$update['final_filename'] = $stored['original'];
$pdfName = $this->fileService->ensurePdfExists(
$stored['stored'],
$stored['extension'],
ExamDraftConfigService::FINAL_UPLOAD_DIR
);
if ($pdfName !== null && ($this->configService->context()['has_final_pdf'] ?? false)) {
$update['final_pdf_file'] = $pdfName;
}
} elseif ($status === 'finalized' && !empty($draft->teacher_file)) {
$copied = $this->fileService->copyDraftToFinal(
(string) $draft->teacher_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;
$pdfName = $this->fileService->ensurePdfExists(
$copied,
pathinfo($copied, PATHINFO_EXTENSION),
ExamDraftConfigService::FINAL_UPLOAD_DIR
);
if ($pdfName !== null && ($this->configService->context()['has_final_pdf'] ?? false)) {
$update['final_pdf_file'] = $pdfName;
}
}
}
if ($status === 'finalized' && ($this->configService->context()['has_legacy'] ?? false)) {
$update['is_legacy'] = 1;
}
$draft->update($update);
return ['ok' => true, 'draft' => $draft->fresh()->toArray()];
}
private function normalizeStatus(string $input, string $default): string
{
$input = strtolower(trim($input));
$aliases = [
'pending' => 'submitted',
'final' => 'finalized',
'approved' => 'finalized',
];
if (isset($aliases[$input])) {
$input = $aliases[$input];
}
return in_array($input, ExamDraftConfigService::STATUS_OPTIONS, true) ? $input : $default;
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Services\Exams;
use App\Models\Configuration;
use Illuminate\Support\Facades\Schema;
class ExamDraftConfigService
{
public const TEACHER_UPLOAD_DIR = 'exams/drafts';
public const FINAL_UPLOAD_DIR = 'exams/finals';
public const MAX_UPLOAD_BYTES = 12 * 1024 * 1024;
public const ALLOWED_EXTENSIONS = ['doc', 'docx'];
public const ADMIN_ALLOWED_EXTENSIONS = ['doc', 'docx', 'pdf'];
public const EXAM_TYPES = [
'Final Exam',
'Midterm Exam',
'Quiz',
'Study Guide',
'Practice Exam',
'Other',
];
public const STATUS_OPTIONS = ['draft', 'submitted', 'reviewed', 'finalized', 'rejected'];
public function context(): array
{
return [
'school_year' => (string) (Configuration::getConfig('school_year') ?? ''),
'semester' => (string) (Configuration::getConfig('semester') ?? ''),
'has_final_pdf' => $this->hasColumn('exam_drafts', 'final_pdf_file'),
'has_legacy' => $this->hasColumn('exam_drafts', 'is_legacy'),
];
}
private function hasColumn(string $table, string $column): bool
{
try {
return Schema::hasColumn($table, $column);
} catch (\Throwable) {
return false;
}
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace App\Services\Exams;
use Illuminate\Http\UploadedFile;
class ExamDraftFileService
{
public function storeUploadedFile(UploadedFile $file, string $subdir, array $allowedExtensions, int $maxBytes): ?array
{
$ext = strtolower($file->getClientOriginalExtension());
if (!in_array($ext, $allowedExtensions, true)) {
return null;
}
if ($file->getSize() > $maxBytes) {
return null;
}
$destination = storage_path('uploads/' . trim($subdir, '/'));
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
$filename = uniqid('exam_', true) . '.' . $ext;
try {
$file->move($destination, $filename);
return [
'stored' => $filename,
'original' => $file->getClientOriginalName(),
'extension' => $ext,
];
} catch (\Throwable) {
return null;
}
}
public function ensurePdfExists(string $finalFilename, ?string $originalExt, string $subdir): ?string
{
$pdfNeighbor = $this->neighborPdfIfExists($finalFilename, $subdir);
if ($pdfNeighbor !== null) {
return $pdfNeighbor;
}
$ext = strtolower((string) $originalExt);
if ($ext === 'pdf') {
$path = $this->fullUploadPath($subdir, $finalFilename);
return is_file($path) ? $finalFilename : null;
}
return $this->convertDocToPdf($this->fullUploadPath($subdir, $finalFilename), $subdir);
}
public function copyDraftToFinal(string $draftFilename, string $sourceDir, string $targetDir): ?string
{
$source = $this->fullUploadPath($sourceDir, $draftFilename);
if (!is_file($source)) {
return null;
}
$destinationDir = storage_path('uploads/' . trim($targetDir, '/'));
if (!is_dir($destinationDir)) {
mkdir($destinationDir, 0755, true);
}
$ext = pathinfo($draftFilename, PATHINFO_EXTENSION);
$destName = uniqid('final_', true) . '.' . $ext;
$destPath = $destinationDir . '/' . $destName;
if (!@copy($source, $destPath)) {
return null;
}
return $destName;
}
private function convertDocToPdf(string $sourcePath, string $targetSubdir): ?string
{
$ext = strtolower(pathinfo($sourcePath, PATHINFO_EXTENSION));
if (!in_array($ext, ['doc', 'docx'], true)) {
return null;
}
$targetDir = storage_path('uploads/' . trim($targetSubdir, '/'));
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
$base = pathinfo($sourcePath, PATHINFO_FILENAME);
$targetPath = $targetDir . '/' . $base . '.pdf';
$cmd = 'soffice --headless --convert-to pdf --outdir ' . escapeshellarg($targetDir) . ' ' . escapeshellarg($sourcePath) . ' 2>/dev/null';
@exec($cmd);
return is_file($targetPath) ? basename($targetPath) : null;
}
private function neighborPdfIfExists(string $filename, string $subdir): ?string
{
if ($filename === '') {
return null;
}
$path = $this->fullUploadPath($subdir, $filename);
$base = pathinfo($path, PATHINFO_FILENAME);
$dir = pathinfo($path, PATHINFO_DIRNAME);
$pdfPath = $dir . '/' . $base . '.pdf';
return is_file($pdfPath) ? basename($pdfPath) : null;
}
private function fullUploadPath(string $subdir, string $filename): string
{
return storage_path('uploads/' . trim($subdir, '/') . '/' . $filename);
}
}
@@ -0,0 +1,133 @@
<?php
namespace App\Services\Exams;
use App\Models\ExamDraft;
use App\Models\TeacherClass;
use Illuminate\Support\Facades\DB;
class ExamDraftTeacherService
{
public function __construct(
private ExamDraftConfigService $configService,
private ExamDraftFileService $fileService
) {
}
public function listForTeacher(int $teacherId): array
{
$context = $this->configService->context();
$schoolYear = (string) ($context['school_year'] ?? '');
$assignments = TeacherClass::getClassAssignmentsByUserId($teacherId, $schoolYear);
$classSectionIds = array_map(static fn ($a) => (int) $a['class_section_id'], $assignments);
$drafts = ExamDraft::query()
->where('teacher_id', $teacherId)
->orderByDesc('created_at')
->get()
->toArray();
$legacyExams = [];
if (($context['has_legacy'] ?? false) && !empty($classSectionIds)) {
$legacyExams = DB::table('exam_drafts as ed')
->select('ed.*', 'cs.class_section_name')
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ed.class_section_id')
->whereIn('ed.class_section_id', $classSectionIds)
->where('ed.is_legacy', 1)
->whereNotNull('ed.final_file')
->orderBy('cs.class_section_name')
->orderByDesc('ed.created_at')
->get()
->map(fn ($r) => (array) $r)
->all();
}
return [
'assignments' => $assignments,
'drafts' => $drafts,
'legacyExams' => $legacyExams,
'examTypes' => ExamDraftConfigService::EXAM_TYPES,
'statusOptions' => ExamDraftConfigService::STATUS_OPTIONS,
'schoolYear' => $schoolYear,
'semester' => (string) ($context['semester'] ?? ''),
];
}
public function store(int $teacherId, array $payload, ?\Illuminate\Http\UploadedFile $file): array
{
$context = $this->configService->context();
$schoolYear = (string) ($context['school_year'] ?? '');
$semester = (string) ($context['semester'] ?? '');
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
$examType = trim((string) ($payload['exam_type'] ?? ''));
$description = trim((string) ($payload['description'] ?? ''));
if ($classSectionId <= 0) {
return ['ok' => false, 'message' => 'Select a class section before submitting.'];
}
$assignment = TeacherClass::query()
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->first();
if (!$assignment) {
return ['ok' => false, 'message' => 'You are not assigned to the selected class section.'];
}
$teacherFile = null;
$teacherFilename = null;
if ($file) {
$stored = $this->fileService->storeUploadedFile(
$file,
ExamDraftConfigService::TEACHER_UPLOAD_DIR,
ExamDraftConfigService::ALLOWED_EXTENSIONS,
ExamDraftConfigService::MAX_UPLOAD_BYTES
);
if ($stored === null) {
return ['ok' => false, 'message' => 'Failed to store the uploaded file.'];
}
$teacherFile = $stored['stored'];
$teacherFilename = $stored['original'];
}
$existingDraft = ExamDraft::query()
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->where('semester', $semester)
->where('school_year', $schoolYear)
->orderByDesc('version')
->first();
$nextVersion = 1;
$previousId = null;
if ($existingDraft) {
$nextVersion = ((int) ($existingDraft->version ?? 1)) + 1;
$previousId = (int) ($existingDraft->id ?? 0) ?: null;
}
$title = $examType !== '' ? $examType : 'Exam Draft';
$draft = ExamDraft::query()->create([
'teacher_id' => $teacherId,
'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,
]);
return [
'ok' => true,
'draft' => $draft->toArray(),
];
}
}