412 lines
15 KiB
PHP
Executable File
412 lines
15 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Models\ClassPrepAdjustment;
|
|
use App\Models\ClassPreparationLog;
|
|
use App\Models\ClassSection;
|
|
use App\Models\Configuration;
|
|
use App\Models\StudentClass;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class ClassPreparationController extends BaseApiController
|
|
{
|
|
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
|
|
) {
|
|
parent::__construct();
|
|
|
|
$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()
|
|
{
|
|
$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;
|
|
}
|
|
|
|
}
|