studentClass = $studentClass; $this->prepLog = $prepLog; $this->classSection = $classSection; $this->config = $config; $this->adjustment = $adjustment; $this->schoolYear = $this->config->getConfig('school_year'); $this->semester = $this->config->getConfig('semester'); } public function index() { $schoolYear = (string) ($this->request->getGet('school_year') ?? $this->schoolYear ?? ''); $semester = (string) ($this->request->getGet('semester') ?? $this->semester ?? ''); $allowed = $this->allowedPrepCategories; $classSections = $this->studentClass->newQuery() ->selectRaw('class_section_id, COUNT(DISTINCT student_id) AS student_count') ->where('school_year', $schoolYear) ->where('semester', $semester) ->groupBy('class_section_id') ->get() ->map(fn ($row) => (array) $row) ->toArray(); $inventoryMap = $this->buildInventoryMap($schoolYear, $semester); $requiredTotals = array_fill_keys($allowed, 0); $results = []; foreach ($classSections as $section) { $classSectionId = (string) ($section['class_section_id'] ?? ''); $studentCount = (int) ($section['student_count'] ?? 0); $classLevel = $this->getClassLevelBySection($classSectionId); $className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; $baseItems = $this->calculatePrepItems($studentCount, $classLevel, $classSectionId); $rawAdjustments = $this->adjustment->newQuery() ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('adjustable', 1) ->get() ->map(fn ($row) => (array) $row) ->toArray(); $adjMap = ['Large Table' => 0, 'Small Table' => 0]; foreach ($rawAdjustments as $adjustment) { $item = $adjustment['item_name'] ?? ''; $delta = (int) ($adjustment['adjustment'] ?? 0); if (array_key_exists($item, $adjMap)) { $adjMap[$item] = $delta; } if (isset($baseItems[$item])) { $baseItems[$item] = max(0, (int) $baseItems[$item] + $delta); } elseif (in_array($item, $allowed, true)) { $baseItems[$item] = max(0, $delta); } } foreach ($allowed as $category) { $requiredTotals[$category] += (int) ($baseItems[$category] ?? 0); } $oldSnap = $this->prepLog->newQuery() ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->orderBy('created_at', 'DESC') ->first(); $oldPrep = $oldSnap ? json_decode($oldSnap['prep_data'] ?? '[]', true) : []; $hasChanged = $this->hasPrepChanged($baseItems, $oldPrep); $results[] = [ 'class_section' => $className, 'class_section_id' => $classSectionId, 'student_count' => $studentCount, 'class_level' => $classLevel, 'prep_items' => $baseItems, 'needs_print' => $hasChanged, 'last_printed_at' => $oldSnap['created_at'] ?? null, 'adjustments' => $adjMap, ]; } $shortages = $this->calculateShortages($requiredTotals, $inventoryMap); return $this->success([ 'schoolYear' => $schoolYear, 'semester' => $semester, 'results' => $results, 'totals' => $requiredTotals, 'shortages' => $shortages, 'available' => $inventoryMap, ], 'Class preparation data retrieved'); } public function saveAdjustment() { $data = $this->request->all(); $sectionId = (string) ($data['class_section_id'] ?? ''); $schoolYear = (string) ($data['school_year'] ?? $this->schoolYear ?? ''); $adjustments = is_array($data['adjustments'] ?? null) ? $data['adjustments'] : []; $updated = 0; foreach ($adjustments as $itemName => $adjustment) { $row = $this->adjustment->newQuery() ->where('class_section_id', $sectionId) ->where('school_year', $schoolYear) ->where('item_name', $itemName) ->first(); $payload = [ 'class_section_id' => $sectionId, 'item_name' => $itemName, 'adjustment' => (int) $adjustment, 'school_year' => $schoolYear, 'adjustable' => 1, ]; if ($row) { $this->adjustment->newQuery() ->where('id', $row['id']) ->update(['adjustment' => $payload['adjustment'], 'adjustable' => 1]); } else { $payload['created_at'] = utc_now(); $this->adjustment->insert($payload); } $updated++; } return $this->success(['updated' => $updated], 'Adjustments saved successfully'); } public function markPrinted() { $schoolYear = (string) ($this->request->getPost('school_year') ?? $this->schoolYear ?? ''); $ids = $this->request->getPost('class_section_ids') ?? []; if (!is_array($ids)) { $ids = $ids ? [$ids] : []; } $ids = array_values(array_unique(array_filter(array_map('strval', $ids)))); $now = utc_now(); $count = 0; foreach ($ids as $classSectionId) { $studentRow = $this->studentClass->newQuery() ->selectRaw('COUNT(DISTINCT student_id) AS cnt') ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('semester', $this->semester) ->first(); $studentCount = (int) ($studentRow['cnt'] ?? 0); $classLevel = $this->getClassLevelBySection((string) $classSectionId); $className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId; $items = $this->calculatePrepItems($studentCount, $classLevel, (string) $classSectionId); $rawAdjustments = $this->adjustment->newQuery() ->where('class_section_id', $classSectionId) ->where('school_year', $schoolYear) ->where('adjustable', 1) ->get() ->map(fn ($row) => (array) $row) ->toArray(); foreach ($rawAdjustments as $adjustment) { $item = $adjustment['item_name'] ?? ''; $delta = (int) ($adjustment['adjustment'] ?? 0); if (isset($items[$item])) { $items[$item] = max(0, (int) $items[$item] + $delta); } } try { $this->prepLog->insert([ 'class_section_id' => (string) $classSectionId, 'class_section' => $className, 'school_year' => $schoolYear, 'prep_data' => json_encode($items), 'created_at' => $now, ]); $count++; } catch (\Throwable $e) { // ignore and continue } } return $this->success(['updated' => $count], 'Marked sections as printed'); } private function calculateShortages(array $requiredTotals, array $inventoryMap): array { $shortages = []; foreach ($requiredTotals as $item => $reqQty) { $have = (int) ($inventoryMap[$item] ?? 0); if ($have < (int) $reqQty) { $shortages[$item] = (int) $reqQty - $have; } } return $shortages; } private function getTeacherCountForSection(string $classSectionId, ?string $schoolYear = null): int { $schoolYear = $schoolYear ?? $this->schoolYear; $row = DB::table('teacher_class') ->selectRaw('COUNT(*) AS cnt') ->where('class_section_id', $classSectionId) ->whereIn('position', ['main', 'ta']) ->where('school_year', $schoolYear) ->first(); return (int) ($row->cnt ?? 0); } private function calculatePrepItems(int $students, int $classLevel, string $classSectionId): array { $allowed = $this->allowedPrepCategories; $items = array_fill_keys($allowed, 0); $categories = DB::table('inventory_categories') ->select('name', 'grade_min', 'grade_max') ->where('type', 'classroom') ->whereIn('name', $allowed) ->orderBy('name') ->get() ->map(fn ($row) => (array) $row) ->toArray(); $isLowerGrades = in_array($classLevel, [1, 2], true); $teacherCount = $this->getTeacherCountForSection($classSectionId); foreach ($categories as $category) { $name = $category['name'] ?? ''; $gradeMin = $category['grade_min']; $gradeMax = $category['grade_max']; if ($gradeMin !== null && $classLevel < (int) $gradeMin) { $items[$name] = 0; continue; } if ($gradeMax !== null && $classLevel > (int) $gradeMax) { $items[$name] = 0; continue; } $qty = 0; switch (strtolower($name)) { case 'small table': $qty = $isLowerGrades ? (int) ceil($students / 3) : 0; break; case 'large table': $qty = $isLowerGrades ? 0 : (int) ceil($students / 4); break; case 'small chair': $qty = $isLowerGrades ? $students : 0; break; case 'regular chair': $qty = $isLowerGrades ? 0 : $students; break; case 'teacher chair': $qty = $teacherCount; break; case 'trash bin': case 'white board': case 'grade box': $qty = 1; break; default: $qty = 0; } if (array_key_exists($name, $items)) { $items[$name] = $qty; } } return $items; } private function hasPrepChanged(array $new, array $old): bool { ksort($new); ksort($old); return json_encode($new) !== json_encode($old); } private function buildInventoryMap(string $schoolYear, string $semester): array { $allowed = $this->allowedPrepCategories; $inventoryMap = array_fill_keys($allowed, 0); $queries = [ DB::table('inventory_items as ii') ->selectRaw('ic.name AS item_name, COALESCE(SUM(ii.good_qty),0) AS available') ->join('inventory_categories as ic', 'ic.id', '=', 'ii.category_id') ->where('ii.type', 'classroom') ->where('ii.school_year', $schoolYear) ->where('ii.semester', $semester) ->whereIn('ic.name', $allowed) ->groupBy('ic.name'), DB::table('inventory_items') ->selectRaw('name AS item_name, COALESCE(SUM(good_qty),0) AS available') ->where('type', 'classroom') ->where('school_year', $schoolYear) ->where('semester', $semester) ->whereIn('name', $allowed) ->groupBy('name'), DB::table('inventory_items as ii') ->selectRaw('ic.name AS item_name, COALESCE(SUM(CASE WHEN ii.`condition`="good" THEN ii.quantity ELSE 0 END),0) AS available') ->join('inventory_categories as ic', 'ic.id', '=', 'ii.category_id') ->where('ii.type', 'classroom') ->where('ii.school_year', $schoolYear) ->where('ii.semester', $semester) ->whereIn('ic.name', $allowed) ->groupBy('ic.name'), DB::table('inventory_items') ->selectRaw('name AS item_name, COALESCE(SUM(CASE WHEN `condition`="good" THEN quantity ELSE 0 END),0) AS available') ->where('type', 'classroom') ->where('school_year', $schoolYear) ->where('semester', $semester) ->whereIn('name', $allowed) ->groupBy('name'), ]; foreach ($queries as $query) { foreach ($query->get() as $row) { $row = (array) $row; $name = (string) ($row['item_name'] ?? $row['name'] ?? ''); $val = (int) ($row['available'] ?? 0); if ($name !== '' && isset($inventoryMap[$name])) { $inventoryMap[$name] = max($inventoryMap[$name], $val); } } } return $inventoryMap; } private function getClassLevelBySection(string $classSectionId): int { if (preg_match('/^(kg|k)(\b|[^a-z0-9])/i', $classSectionId)) { return 1; } $numeric = (int) preg_replace('/\D/', '', $classSectionId); if ($numeric > 0 && $numeric < 30) { $firstDigit = (int) substr((string) $numeric, 0, 1); return match ($firstDigit) { 1 => 1, 2 => 2, default => 3, }; } return 3; } }