update controllers logic
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user