add class progress and fix endpoints
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user