Files
alrahma_sunday_school_api/app/Http/Controllers/View/ClassPreparationController.php
T
2026-03-05 12:29:37 -05:00

546 lines
20 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\View;
use App\Http\Controllers\Controller;
use App\Models\ClassPrepAdjustment;
use App\Models\ClassPreparationLog;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\StudentClass;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class ClassPreparationController extends Controller
{
protected StudentClass $studentClass;
protected ClassPreparationLog $prepLog;
protected ClassSection $classSection;
protected Configuration $config;
protected ClassPrepAdjustment $adjustment;
protected ?string $schoolYear;
protected ?string $semester;
private array $allowedPrepCategories = [
'Grade Box',
'Large Table',
'Regular Chair',
'Small Chair',
'Small Table',
'Teacher Chair',
'Trash Bin',
'White Board',
];
public function __construct(
StudentClass $studentClass,
ClassPreparationLog $prepLog,
ClassSection $classSection,
Configuration $config,
ClassPrepAdjustment $adjustment
) {
$this->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(Request $request): View
{
$schoolYear = (string) ($request->query('school_year') ?? $this->schoolYear ?? '');
$semParam = $request->query('semester');
$semester = is_string($semParam) && $semParam !== '' ? $semParam : (string) ($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);
$prepResults = [];
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);
$prepResults[] = [
'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 = [];
foreach ($requiredTotals as $item => $required) {
$available = (int) ($inventoryMap[$item] ?? 0);
if ($available < (int) $required) {
$shortages[$item] = (int) $required - $available;
}
}
return view('class_prep/list', [
'prepResults' => $prepResults,
'schoolYear' => $schoolYear,
'semester' => $semester,
'shortages' => $shortages,
'totalNeeded' => $requiredTotals,
'available' => $inventoryMap,
]);
}
public function saveAdjustment(Request $request): RedirectResponse
{
$data = $request->all();
$sectionId = (string) ($data['class_section_id'] ?? '');
$schoolYear = (string) ($data['school_year'] ?? $this->schoolYear ?? '');
$adjustments = is_array($data['adjustments'] ?? null) ? $data['adjustments'] : [];
foreach ($adjustments as $itemName => $adjustment) {
$row = $this->adjustment->newQuery()
->where('class_section_id', $sectionId)
->where('school_year', $schoolYear)
->where('item_name', $itemName)
->first();
if ($row) {
$this->adjustment->update($row['id'], [
'adjustment' => (int) $adjustment,
'adjustable' => 1,
]);
continue;
}
$this->adjustment->insert([
'class_section_id' => $sectionId,
'item_name' => $itemName,
'adjustment' => (int) $adjustment,
'school_year' => $schoolYear,
'adjustable' => 1,
]);
}
return redirect()->to('/class-prep?school_year=' . urlencode($schoolYear))
->with('success', 'Adjustments saved successfully.');
}
public function print(string $classSectionId, string $schoolYear): View
{
$className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $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($classSectionId);
$items = $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();
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);
}
}
$this->prepLog->insert([
'class_section_id' => $classSectionId,
'class_section' => $className,
'school_year' => $schoolYear,
'prep_data' => json_encode($items),
'created_at' => utc_now(),
]);
return view('class_prep/print', [
'classSectionId' => $classSectionId,
'classSection' => $className,
'schoolYear' => $schoolYear,
'prepItems' => $items,
]);
}
public function apiList(Request $request): JsonResponse
{
$schoolYear = (string) ($request->query('school_year') ?? $this->schoolYear ?? '');
$allowed = $this->allowedPrepCategories;
$classSections = $this->studentClass->newQuery()
->selectRaw('class_section_id, COUNT(DISTINCT student_id) AS student_count')
->where('school_year', $schoolYear)
->where('semester', $this->semester)
->groupBy('class_section_id')
->get()
->map(fn ($row) => (array) $row)
->toArray();
$inventoryMap = $this->buildInventoryMap($schoolYear, $this->semester);
$requiredTotals = array_fill_keys($allowed, 0);
$results = [];
foreach ($classSections as $row) {
$classSectionId = (string) ($row['class_section_id'] ?? '');
$studentCount = (int) ($row['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);
}
}
foreach ($allowed as $cat) {
$requiredTotals[$cat] += (int) ($baseItems[$cat] ?? 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 = [];
foreach ($requiredTotals as $item => $required) {
$available = (int) ($inventoryMap[$item] ?? 0);
if ($available < (int) $required) {
$shortages[$item] = (int) $required - $available;
}
}
return response()->json([
'ok' => true,
'schoolYear' => $schoolYear,
'results' => $results,
'totals' => $requiredTotals,
'shortages' => $shortages,
'csrf_token' => csrf_token(),
'csrf_hash' => csrf_token(),
]);
}
public function apiMarkPrinted(Request $request): JsonResponse
{
$schoolYear = (string) ($request->post('school_year') ?? $this->schoolYear ?? '');
$ids = $request->post('class_section_ids', []);
if (!is_array($ids)) {
$ids = $ids ? [$ids] : [];
}
$ids = array_values(array_unique(array_filter(array_map('strval', $ids))));
$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($classSectionId);
$className = $this->classSection->getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
$items = $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();
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' => $classSectionId,
'class_section' => $className,
'school_year' => $schoolYear,
'prep_data' => json_encode($items),
'created_at' => utc_now(),
]);
$count++;
} catch (\Throwable $e) {
// ignore and continue to next section
}
}
return response()->json([
'ok' => true,
'updated' => $count,
'csrf_token' => csrf_token(),
'csrf_hash' => csrf_token(),
]);
}
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()->toArray() 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;
}
}