update controllers logic

This commit is contained in:
root
2026-04-23 00:04:35 -04:00
parent 1977a513df
commit ca4ba272fc
353 changed files with 13402 additions and 1301 deletions
@@ -2,7 +2,9 @@
namespace App\Services\ClassProgress;
use App\Models\Configuration;
use App\Models\SubjectCurriculum;
use App\Services\SemesterRangeService;
class ClassProgressMetaService
{
@@ -16,22 +18,126 @@ class ClassProgressMetaService
return (array) config('progress.status_options', []);
}
/**
* Fallback: next N Sundays from today (CI `buildUpcomingSundayOptions`).
*
* @return list<string>
*/
public function sundayOptions(int $count = 12): array
{
$start = now();
if ((int) $start->format('w') !== 0) {
$start = $start->next('Sunday');
return $this->buildUpcomingSundayOptions($count);
}
/**
* CI-aligned Sundays within semester/school-year range when configuration allows.
*
* @return list<string>
*/
public function sundayOptionsAlignedWithCi(int $count = 12): array
{
$range = $this->resolveProgressDateRange();
if ($range === null) {
return $this->buildUpcomingSundayOptions($count);
}
[$rangeStart, $rangeEnd] = $range;
try {
$start = new \DateTime($rangeStart);
$end = new \DateTime($rangeEnd);
} catch (\Exception $e) {
return $this->buildUpcomingSundayOptions($count);
}
if ($end < $start) {
[$start, $end] = [$end, $start];
}
$weekday = (int) $start->format('w');
if ($weekday !== 0) {
$start->modify('next sunday');
}
$options = [];
while ($start <= $end) {
$options[] = $start->format('Y-m-d');
$start->modify('+7 days');
}
return $options !== [] ? $options : $this->buildUpcomingSundayOptions($count);
}
/** CI `pickDefaultWeekStart`. */
public function pickDefaultWeekStart(array $options): string
{
if ($options === []) {
return '';
}
$today = date('Y-m-d');
$default = '';
foreach ($options as $option) {
if ($option <= $today) {
$default = $option;
continue;
}
break;
}
return $default !== '' ? $default : $options[0];
}
/**
* @return list<string>
*/
private function buildUpcomingSundayOptions(int $count): array
{
$start = new \DateTime('today');
$weekday = (int) $start->format('w');
if ($weekday !== 0) {
$start->modify('next sunday');
}
$options = [];
for ($i = 0; $i < $count; $i++) {
$options[] = $start->format('Y-m-d');
$start = $start->addDays(7);
$start->modify('+7 days');
}
return $options;
}
/**
* CI `resolveProgressDateRange`.
*
* @return array{0:string,1:string}|null
*/
private function resolveProgressDateRange(): ?array
{
$schoolYear = (string) (Configuration::getConfig('school_year') ?? '');
if ($schoolYear === '') {
return null;
}
/** @var SemesterRangeService $svc */
$svc = app(SemesterRangeService::class);
$semester = $svc->normalizeSemester((string) (Configuration::getConfig('semester') ?? ''));
if ($semester === '') {
$semester = $svc->getSemesterForDate();
}
if ($semester !== '') {
$range = $svc->getSemesterRange($schoolYear, $semester);
if ($range !== null) {
return $range;
}
}
return $svc->getSchoolYearRange($schoolYear);
}
public function curriculumOptions(?int $classId): array
{
if (!$classId) {
@@ -2,10 +2,11 @@
namespace App\Services\ClassProgress;
use App\Models\ClassProgressAttachment;
use App\Models\ClassProgressReport;
use App\Models\TeacherClass;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -13,7 +14,8 @@ class ClassProgressMutationService
{
public function __construct(
private ClassProgressRuleService $rules,
private ClassProgressAttachmentService $attachments
private ClassProgressAttachmentService $attachments,
private ClassProgressQueryService $queries,
) {}
public function createReports(User $user, array $payload, array $filesBySubject): Collection
@@ -30,7 +32,31 @@ class ClassProgressMutationService
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) {
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, $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) {
@@ -83,6 +109,160 @@ class ClassProgressMutationService
return $reports;
}
/**
* CI `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);
$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,
$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 ($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) {
@@ -4,16 +4,13 @@ namespace App\Services\ClassProgress;
use App\Models\ClassProgressAttachment;
use App\Models\ClassProgressReport;
use App\Models\Configuration;
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()
@@ -21,8 +18,13 @@ class ClassProgressQueryService
->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'])) {
if (! empty($filters['class_section_id'])) {
$ids = $this->resolveAssignedTeacherIds((int) $filters['class_section_id']);
$query->whereIn('teacher_id', $ids !== [] ? $ids : [(int) $user->id]);
} else {
$query->where('teacher_id', $user->id);
}
} elseif (! empty($filters['teacher_id'])) {
$query->where('teacher_id', (int) $filters['teacher_id']);
}
@@ -53,12 +55,17 @@ class ClassProgressQueryService
public function getReport(User $user, int $reportId): ClassProgressReport
{
$query = ClassProgressReport::query()->with('classSection');
if (!$this->isAdmin($user)) {
$query->where('teacher_id', $user->id);
$report = ClassProgressReport::query()->with('classSection')->findOrFail($reportId);
if ($this->isAdmin($user)) {
return $report;
}
return $query->findOrFail($reportId);
if ($this->viewerMayAccessReport($user, $report)) {
return $report;
}
abort(404);
}
public function weeklyReports(User $user, ClassProgressReport $report): array
@@ -69,8 +76,9 @@ class ClassProgressQueryService
->whereDate('week_start', $report->week_start)
->orderBy('subject', 'asc');
if (!$this->isAdmin($user)) {
$query->where('teacher_id', $user->id);
if (! $this->isAdmin($user)) {
$ids = $this->resolveAssignedTeacherIds((int) $report->class_section_id);
$query->whereIn('teacher_id', $ids !== [] ? $ids : [(int) $user->id]);
}
$rows = $query->get();
@@ -137,4 +145,40 @@ class ClassProgressQueryService
return false;
}
/**
* CI `ClassProgressController::resolveAssignedTeacherIds`.
*
* @return list<int>
*/
public function resolveAssignedTeacherIds(int $classSectionId): array
{
$schoolYear = (string) (Configuration::getConfig('school_year') ?? '');
if ($schoolYear === '' || $classSectionId <= 0) {
return [];
}
$semester = (string) (Configuration::getConfig('semester') ?? '');
$rows = TeacherClass::assignedForSectionTerm($classSectionId, $semester, $schoolYear);
$ids = [];
foreach ($rows as $row) {
$id = (int) ($row['teacher_id'] ?? 0);
if ($id > 0) {
$ids[$id] = true;
}
}
return array_map('intval', array_keys($ids));
}
private function viewerMayAccessReport(User $user, ClassProgressReport $report): bool
{
if ((int) $report->teacher_id === (int) $user->id) {
return true;
}
$ids = $this->resolveAssignedTeacherIds((int) $report->class_section_id);
return $ids !== [] && in_array((int) $user->id, $ids, true);
}
}
@@ -4,6 +4,9 @@ namespace App\Services\ClassProgress;
class ClassProgressRuleService
{
/** CI `ClassProgressController::CUSTOM_UNIT_ROW_LABEL` — must match teacher form JS. */
public const CUSTOM_UNIT_ROW_LABEL = 'Custom';
public function defaultStatus(): string
{
return 'on_track';
@@ -15,6 +18,12 @@ class ClassProgressRuleService
return $values === [] ? null : $values;
}
/**
* CI `buildUnitChapterSummary` — Custom / typed chapter rows preserved.
*
* @param array<int|string, mixed> $unitValues
* @param array<int|string, mixed> $chapterValues
*/
public function buildUnitChapterSummary(array $unitValues, array $chapterValues): ?string
{
$parts = [];
@@ -25,9 +34,12 @@ class ClassProgressRuleService
if ($unit === '' && $chapter === '') {
continue;
}
if (strcasecmp($unit, self::CUSTOM_UNIT_ROW_LABEL) === 0 && $chapter !== '') {
$unit = self::CUSTOM_UNIT_ROW_LABEL;
}
$segment = $unit;
if ($chapter !== '') {
$segment = $segment !== '' ? $segment . ' / ' . $chapter : $chapter;
$segment = $segment !== '' ? $unit.' / '.$chapter : $chapter;
}
if ($segment === '') {
continue;
@@ -40,9 +52,25 @@ class ClassProgressRuleService
}
$summary = implode(' ; ', $parts);
return mb_strlen($summary) > 120 ? mb_substr($summary, 0, 120) : $summary;
}
/** CI `hasIslamicUnitSelection`. */
public function hasIslamicUnitSelection(array $payload): bool
{
$unitValues = array_map('trim', (array) ($payload['unit_islamic'] ?? []));
$chapterValues = array_map('trim', (array) ($payload['chapter_islamic'] ?? []));
$count = max(count($unitValues), count($chapterValues));
for ($i = 0; $i < $count; $i++) {
if (($unitValues[$i] ?? '') !== '' || ($chapterValues[$i] ?? '') !== '') {
return true;
}
}
return false;
}
public function ensureWeekEnd(string $weekStart, ?string $weekEnd): string
{
if ($weekEnd) {
@@ -62,4 +90,47 @@ class ClassProgressRuleService
{
return strtotime($weekEnd) >= strtotime($weekStart);
}
/**
* CI `parseUnitChapterSummary` — inverse of {@see buildUnitChapterSummary()} for edit forms.
*
* @return array{units: list<string>, chapters: list<string>}
*/
public function parseUnitChapterSummary(string $summary): array
{
$summary = trim($summary);
if ($summary === '') {
return ['units' => [], 'chapters' => []];
}
$units = [];
$chapters = [];
$segments = preg_split('/\s*;\s*/', $summary, -1, PREG_SPLIT_NO_EMPTY);
foreach ($segments as $segment) {
$segment = trim((string) $segment);
if ($segment === '') {
continue;
}
if (preg_match('/^Custom\s*\/\s*(.+)$/iu', $segment, $m)) {
$units[] = self::CUSTOM_UNIT_ROW_LABEL;
$chapters[] = trim($m[1]);
continue;
}
$parts = preg_split('/\s*\/\s*/', $segment, 2);
if (count($parts) === 2) {
$u = trim($parts[0]);
if (strcasecmp($u, self::CUSTOM_UNIT_ROW_LABEL) === 0) {
$u = self::CUSTOM_UNIT_ROW_LABEL;
}
$units[] = $u;
$chapters[] = trim($parts[1]);
} else {
$units[] = $segment;
$chapters[] = '';
}
}
return ['units' => $units, 'chapters' => $chapters];
}
}