buildTeacherClassContext($userId); if (! $ctx['ok']) { return $ctx; } $classIds = $ctx['classSectionIds']; $lockClassSection = count($classIds) === 1; $selectedClassId = $lockClassSection ? (int) ($ctx['defaultClassSectionId'] ?? 0) : (int) ($classSectionId ?? ($ctx['defaultClassSectionId'] ?? 0)); $classID = null; if (! empty($selectedClassId)) { $classID = ClassSection::getClassId($selectedClassId); $classID = $classID !== null ? (int) $classID : null; } $builder = DB::table('inventory_items as i') ->select('i.id', 'i.name', 'i.isbn', 'i.edition', 'i.quantity', 'i.category_id', 'i.type', 'c.name as category_name', 'c.grade_min', 'c.grade_max') ->leftJoin('inventory_categories as c', 'c.id', '=', 'i.category_id') ->where('i.type', 'book'); if (! empty($classID)) { $builder->where(function ($q) use ($classID) { $q->where(function ($inner) use ($classID) { $inner->whereNotNull('c.grade_min') ->whereNotNull('c.grade_max') ->where('c.grade_min', '<=', $classID) ->where('c.grade_max', '>=', $classID); })->orWhere(function ($inner) use ($classID) { $inner->whereNotNull('c.grade_min') ->whereNull('c.grade_max') ->where('c.grade_min', $classID); })->orWhere(function ($inner) use ($classID) { $inner->whereNotNull('c.grade_max') ->whereNull('c.grade_min') ->where('c.grade_max', $classID); }); }); } $rows = $builder->orderBy('i.name')->get()->map(fn ($r) => (array) $r)->all(); $labelFor = static function (?array $r): string { $name = $r['category_name'] ?? 'Uncategorized'; $gmin = $r['grade_min'] ?? null; $gmax = $r['grade_max'] ?? null; if ($gmin === null && $gmax === null) { return $name; } if ($gmin !== null && $gmax !== null) { return $name.' (G'.(int) $gmin.'–G'.(int) $gmax.')'; } if ($gmin !== null) { return $name.' (G'.(int) $gmin.'+)'; } return $name.' (≤G'.(int) $gmax.')'; }; $booksGrouped = []; foreach ($rows as $r) { $catId = (int) ($r['category_id'] ?? 0); if (! isset($booksGrouped[$catId])) { $booksGrouped[$catId] = [ 'label' => $labelFor($r), 'items' => [], ]; } $display = $r['name']; if (! empty($r['isbn'])) { $display .= ' — ISBN '.$r['isbn']; } if (! empty($r['edition'])) { $display .= ' ('.$r['edition'].')'; } $booksGrouped[$catId]['items'][] = [ 'id' => (int) $r['id'], 'display' => $display, 'quantity' => (int) ($r['quantity'] ?? 0), ]; } uasort($booksGrouped, fn ($a, $b) => strnatcasecmp($a['label'], $b['label'])); foreach ($booksGrouped as &$grp) { usort($grp['items'], fn ($x, $y) => strnatcasecmp($x['display'], $y['display'])); } unset($grp); $students = $ctx['studentsData'][$selectedClassId] ?? []; $already = []; $itemId = (int) ($itemId ?? 0); if ($itemId && $selectedClassId) { $rowsHist = InventoryMovement::query() ->selectRaw('student_id, SUM(CASE WHEN qty_change < 0 THEN -qty_change ELSE 0 END) AS qty') ->where([ 'item_id' => $itemId, 'movement_type' => 'distribution', 'class_section_id' => $selectedClassId, 'school_year' => $ctx['schoolYear'], ]) ->groupBy('student_id') ->get() ->map(fn ($r) => (array) $r) ->all(); foreach ($rowsHist as $r) { $sid = (int) ($r['student_id'] ?? 0); if ($sid) { $already[$sid] = (int) ($r['qty'] ?? 0); } } } $onHand = 0; if ($itemId) { $book = InventoryItem::query()->find($itemId); if ($book && $book->type === 'book') { $onHand = (int) ($book->quantity ?? 0); } } return [ 'ok' => true, 'booksGrouped' => $booksGrouped, 'items' => $rows, 'classes' => $ctx['classes'], 'class_section_id' => $selectedClassId, 'lockClassSection' => $lockClassSection, 'item_id' => $itemId, 'students' => $students, 'already' => $already, 'onHand' => $onHand, 'schoolYear' => $ctx['schoolYear'], 'semester' => $ctx['semester'], ]; } public function distribute(int $userId, array $payload): array { $ctx = $this->buildTeacherClassContext($userId); if (! $ctx['ok']) { return $ctx; } $itemId = (int) ($payload['item_id'] ?? 0); $postedClassId = (int) ($payload['class_section_id'] ?? 0); $selectedIds = array_values(array_unique(array_map('intval', $payload['student_ids'] ?? []))); $note = (string) ($payload['note'] ?? ''); $classIds = $ctx['classSectionIds']; $classSectionId = (count($classIds) === 1) ? (int) $classIds[0] : (in_array($postedClassId, $classIds, true) ? $postedClassId : 0); if (! $classSectionId) { return ['ok' => false, 'message' => 'Invalid class selection.']; } $book = InventoryItem::query()->find($itemId); if (! $book || $book->type !== 'book') { return ['ok' => false, 'message' => 'Invalid book.']; } $students = $ctx['studentsData'][$classSectionId] ?? []; $validIds = []; foreach ($students as $st) { $sid = $st['student_id'] ?? ($st['id'] ?? null); if ($sid) { $validIds[(int) $sid] = true; } } $selectedIds = array_values(array_filter($selectedIds, fn ($sid) => isset($validIds[$sid]))); $already = []; $rows = InventoryMovement::query() ->selectRaw('student_id, SUM(CASE WHEN qty_change < 0 THEN -qty_change ELSE 0 END) AS qty') ->where([ 'item_id' => $itemId, 'movement_type' => 'distribution', 'class_section_id' => $classSectionId, 'school_year' => $ctx['schoolYear'], ]) ->groupBy('student_id') ->get() ->map(fn ($r) => (array) $r) ->all(); foreach ($rows as $r) { $sid = (int) ($r['student_id'] ?? 0); if ($sid) { $already[$sid] = (int) ($r['qty'] ?? 0); } } $toGive = []; foreach ($selectedIds as $sid) { if (($already[$sid] ?? 0) < 1) { $toGive[] = $sid; } } $needed = count($toGive); $onHand = (int) ($book->quantity ?? 0); if ($needed > $onHand) { return ['ok' => false, 'message' => 'Not enough stock to complete distribution.']; } $okCount = 0; foreach ($toGive as $sid) { $ok = $this->movementService->recordMovement($itemId, -1, 'distribution', 'Teacher distribution', $note, $classSectionId, $sid, $userId); if ($ok) { $okCount++; } } return [ 'ok' => true, 'distributed' => $okCount, 'already_assigned' => count($selectedIds) - $okCount, ]; } private function buildTeacherClassContext(int $userId): array { if ($userId <= 0) { return ['ok' => false, 'message' => 'Please log in first.']; } $classes = TeacherClass::getClassAssignmentsByUserId($userId, $this->context->schoolYear()); if (empty($classes)) { return ['ok' => false, 'message' => 'You do not have an assigned class yet.']; } $user = User::query()->find($userId); if (! $user) { return ['ok' => false, 'message' => 'User not found.']; } $roles = DB::table('user_roles as ur') ->join('roles as r', 'r.id', '=', 'ur.role_id') ->where('ur.user_id', $userId) ->pluck('r.name') ->toArray(); if (! in_array('teacher', $roles, true) && ! in_array('teacher_assistant', $roles, true)) { return ['ok' => false, 'message' => 'Access denied.']; } $classSectionIds = array_column($classes, 'class_section_id'); $studentsData = []; if (! empty($classSectionIds)) { $students = StudentClass::getStudentsByClassSectionIds($classSectionIds); foreach ($students as $student) { $studentsData[$student['class_section_id']][] = $student; } } $taAssignments = TeacherClass::query() ->whereIn('class_section_id', $classSectionIds) ->where('school_year', $this->context->schoolYear()) ->where('position', 'ta') ->get() ->toArray(); $taNames = []; foreach ($taAssignments as $ta) { $taUser = User::query()->find($ta['teacher_id'] ?? 0); if ($taUser) { $csid = $ta['class_section_id']; $taNames[$csid][] = trim(($taUser->firstname ?? '').' '.($taUser->lastname ?? '')); } } $defaultClassSectionId = $classSectionIds[0] ?? null; return [ 'ok' => true, 'classes' => $classes, 'classSectionIds' => $classSectionIds, 'studentsData' => $studentsData, 'taNames' => $taNames, 'defaultClassSectionId' => $defaultClassSectionId, 'schoolYear' => $this->context->schoolYear(), 'semester' => $this->context->semester(), ]; } }