388 lines
15 KiB
PHP
388 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Services\ClassProgress;
|
|
|
|
use App\Models\ClassProgressAttachment;
|
|
use App\Models\ClassProgressReport;
|
|
use App\Models\TeacherClass;
|
|
use App\Models\User;
|
|
use App\Services\SchoolYears\SchoolYearContextService;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class ClassProgressMutationService
|
|
{
|
|
public function __construct(
|
|
private ClassProgressRuleService $rules,
|
|
private ClassProgressAttachmentService $attachments,
|
|
private ClassProgressQueryService $queries,
|
|
private SchoolYearContextService $schoolYears,
|
|
) {}
|
|
|
|
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);
|
|
|
|
$schoolYear = $this->schoolYears->currentSchoolYear($payload['school_year'] ?? null);
|
|
$semester = $this->schoolYears->currentSemester($payload['semester'] ?? null);
|
|
|
|
$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.');
|
|
}
|
|
|
|
if (!$this->rules->hasIslamicUnitSelection($payload)) {
|
|
throw new \InvalidArgumentException('Please select at least one Islamic Studies unit.');
|
|
}
|
|
|
|
$confirmOverwrite = filter_var($payload['confirm_overwrite'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
$existingIds = ClassProgressReport::query()
|
|
->where('class_section_id', $classSectionId)
|
|
->whereDate('week_start', $weekStart)
|
|
->where('teacher_id', $user->id)
|
|
->pluck('id')
|
|
->map(fn ($id) => (int) $id)
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
if ($existingIds !== [] && !$confirmOverwrite) {
|
|
throw new \InvalidArgumentException('CONFIRM_OVERWRITE');
|
|
}
|
|
|
|
$reports = DB::transaction(function () use ($user, $payload, $subjectSections, $classSectionId, $schoolYear, $semester, $weekStart, $weekEnd, $filesBySubject, $confirmOverwrite, $existingIds) {
|
|
if ($existingIds !== [] && $confirmOverwrite) {
|
|
ClassProgressAttachment::query()->whereIn('report_id', $existingIds)->delete();
|
|
ClassProgressReport::query()->whereIn('id', $existingIds)->delete();
|
|
}
|
|
|
|
$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);
|
|
|
|
$data = [
|
|
'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),
|
|
];
|
|
|
|
if (Schema::hasColumn('class_progress_reports', 'school_year')) {
|
|
$data['school_year'] = $schoolYear !== '' ? $schoolYear : null;
|
|
}
|
|
if (Schema::hasColumn('class_progress_reports', 'semester')) {
|
|
$data['semester'] = $semester !== '' ? $semester : null;
|
|
}
|
|
|
|
$report = ClassProgressReport::query()->create($data);
|
|
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* legacy `ClassProgressController::update` — sync all subjects for the weekly batch keyed by {@see $anchor}.
|
|
*
|
|
* @return Collection<int, ClassProgressReport>
|
|
*/
|
|
public function syncWeeklyFromTeacherForm(User $user, ClassProgressReport $anchor, array $payload, array $filesBySubject): Collection
|
|
{
|
|
$subjectSections = (array) config('progress.subject_sections', []);
|
|
$classSectionId = (int) ($payload['class_section_id'] ?? $anchor->class_section_id);
|
|
|
|
$this->assertTeacherAssignment($user, $classSectionId);
|
|
|
|
$schoolYear = $this->schoolYears->currentSchoolYear($payload['school_year'] ?? null);
|
|
$semester = $this->schoolYears->currentSemester($payload['semester'] ?? null);
|
|
|
|
$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.');
|
|
}
|
|
|
|
if (! $this->rules->hasIslamicUnitSelection($payload)) {
|
|
throw new \InvalidArgumentException('Please select at least one Islamic Studies unit.');
|
|
}
|
|
|
|
$originalWeekStart = $anchor->week_start instanceof \Carbon\CarbonInterface
|
|
? $anchor->week_start->format('Y-m-d')
|
|
: (string) $anchor->week_start;
|
|
|
|
$confirmOverwrite = filter_var($payload['confirm_overwrite'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
|
|
|
if ($weekStart !== '' && $weekStart !== $originalWeekStart) {
|
|
$conflicts = ClassProgressReport::query()
|
|
->where('class_section_id', $classSectionId)
|
|
->whereDate('week_start', $weekStart)
|
|
->where('teacher_id', $user->id)
|
|
->pluck('id')
|
|
->map(fn ($id) => (int) $id)
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
if ($conflicts !== [] && ! $confirmOverwrite) {
|
|
throw new \InvalidArgumentException('CONFIRM_OVERWRITE');
|
|
}
|
|
|
|
if ($conflicts !== [] && $confirmOverwrite) {
|
|
ClassProgressAttachment::query()->whereIn('report_id', $conflicts)->delete();
|
|
ClassProgressReport::query()->whereIn('id', $conflicts)->delete();
|
|
}
|
|
}
|
|
|
|
$allowedIds = $this->queries->resolveAssignedTeacherIds($classSectionId);
|
|
if ($allowedIds === []) {
|
|
$allowedIds = [(int) $user->id];
|
|
}
|
|
|
|
return DB::transaction(function () use (
|
|
$user,
|
|
$anchor,
|
|
$payload,
|
|
$subjectSections,
|
|
$classSectionId,
|
|
$weekStart,
|
|
$weekEnd,
|
|
$schoolYear,
|
|
$semester,
|
|
$filesBySubject,
|
|
$allowedIds,
|
|
$originalWeekStart,
|
|
): Collection {
|
|
$weeklyBatch = ClassProgressReport::query()
|
|
->whereIn('teacher_id', $allowedIds)
|
|
->where('class_section_id', $classSectionId)
|
|
->whereDate('week_start', $originalWeekStart)
|
|
->orderBy('subject')
|
|
->get();
|
|
|
|
$reportMap = [];
|
|
foreach ($weeklyBatch as $rep) {
|
|
$subject = (string) ($rep->subject ?? '');
|
|
if ($subject !== '') {
|
|
$reportMap[$subject] = $rep;
|
|
}
|
|
}
|
|
|
|
$updated = collect();
|
|
$flagsPresent = array_key_exists('flags', $payload);
|
|
|
|
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);
|
|
|
|
$subjectName = $section['db_subject'] ?? $section['label'] ?? $slug;
|
|
$existing = $reportMap[$subjectName] ?? null;
|
|
|
|
if ($unitTitle === null && $existing) {
|
|
$unitTitle = $existing->unit_title;
|
|
}
|
|
|
|
$data = [
|
|
'class_section_id' => $classSectionId,
|
|
'week_start' => $weekStart,
|
|
'week_end' => $weekEnd,
|
|
'subject' => $subjectName,
|
|
'unit_title' => $unitTitle,
|
|
'covered' => $covered,
|
|
'homework' => $homework !== '' ? $homework : null,
|
|
];
|
|
|
|
if ($flagsPresent) {
|
|
$data['flags_json'] = $this->rules->normalizeFlags($payload['flags'] ?? null);
|
|
}
|
|
|
|
if (Schema::hasColumn('class_progress_reports', 'school_year')) {
|
|
$data['school_year'] = $schoolYear !== '' ? $schoolYear : null;
|
|
}
|
|
if (Schema::hasColumn('class_progress_reports', 'semester')) {
|
|
$data['semester'] = $semester !== '' ? $semester : null;
|
|
}
|
|
|
|
if ($existing) {
|
|
$existing->fill($data);
|
|
$existing->save();
|
|
$saved = $existing->fresh();
|
|
} else {
|
|
$data['teacher_id'] = $user->id;
|
|
$data['status'] = $this->rules->defaultStatus();
|
|
$saved = ClassProgressReport::query()->create($data);
|
|
}
|
|
|
|
if (! $saved) {
|
|
continue;
|
|
}
|
|
|
|
$updated->push($saved);
|
|
|
|
$attachments = $filesBySubject[$slug] ?? [];
|
|
$stored = $this->attachments->storeAttachments((int) $saved->id, $attachments);
|
|
if ($stored !== [] && ! $saved->attachment_path) {
|
|
$saved->update(['attachment_path' => $stored[0]['file_path'] ?? null]);
|
|
}
|
|
}
|
|
|
|
if ($updated->isEmpty()) {
|
|
throw new \InvalidArgumentException('Please provide progress for at least one subject.');
|
|
}
|
|
|
|
Log::info('Class progress weekly sync completed.', [
|
|
'teacher_id' => $user->id,
|
|
'anchor_id' => $anchor->id,
|
|
'count' => $updated->count(),
|
|
]);
|
|
|
|
return $updated;
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|