add controllers, servoices
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassPreparation;
|
||||
|
||||
use App\Models\ClassPrepAdjustment;
|
||||
|
||||
class ClassPreparationAdjustmentService
|
||||
{
|
||||
public function applyAdjustments(array $baseItems, string $classSectionId, string $schoolYear): array
|
||||
{
|
||||
$rawAdjustments = ClassPrepAdjustment::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('adjustable', 1)
|
||||
->get()
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
return [$baseItems, $adjMap];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassPreparation;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ClassPreparationCalculatorService
|
||||
{
|
||||
private array $allowedPrepCategories = [
|
||||
'Grade Box',
|
||||
'Large Table',
|
||||
'Regular Chair',
|
||||
'Small Chair',
|
||||
'Small Table',
|
||||
'Teacher Chair',
|
||||
'Trash Bin',
|
||||
'White Board',
|
||||
];
|
||||
|
||||
public function getAllowedCategories(): array
|
||||
{
|
||||
return $this->allowedPrepCategories;
|
||||
}
|
||||
|
||||
public function getClassLevelBySection(string $classSectionId): int
|
||||
{
|
||||
$sectionName = ClassSection::getClassSectionNameBySectionId($classSectionId);
|
||||
$label = $sectionName !== null && $sectionName !== '' ? (string) $sectionName : $classSectionId;
|
||||
|
||||
if (preg_match('/^(kg|k)(\\b|[^a-z0-9])/i', $label)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$numValue = (int) preg_replace('/\\D/', '', $label);
|
||||
|
||||
if ($numValue > 0 && $numValue < 30) {
|
||||
$firstDigit = (int) substr((string) $numValue, 0, 1);
|
||||
return $firstDigit === 1 ? 1 : ($firstDigit === 2 ? 2 : 3);
|
||||
}
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
public 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()
|
||||
->toArray();
|
||||
|
||||
$isLowerGrades = in_array($classLevel, [1, 2], true);
|
||||
$teacherCount = $this->getTeacherCountForSection(
|
||||
$classSectionId,
|
||||
(string) Configuration::getConfig('school_year')
|
||||
);
|
||||
|
||||
foreach ($categories as $cat) {
|
||||
$name = (string) $cat->name;
|
||||
$gradeMin = $cat->grade_min ?? null;
|
||||
$gradeMax = $cat->grade_max ?? null;
|
||||
|
||||
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 = (int) $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 getTeacherCountForSection(string $classSectionId, ?string $schoolYear = null): int
|
||||
{
|
||||
$schoolYear = $schoolYear ?: (string) Configuration::getConfig('school_year');
|
||||
|
||||
$row = DB::table('teacher_class')
|
||||
->select('COUNT(*) AS cnt')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->whereIn('position', ['main', 'ta'])
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
return (int) ($row->cnt ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassPreparation;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ClassPreparationContextService
|
||||
{
|
||||
/** cache for roster presence per term */
|
||||
private array $rosterPresenceCache = [];
|
||||
|
||||
public function getSchoolYear(): string
|
||||
{
|
||||
return (string) (Configuration::getConfig('school_year') ?? '');
|
||||
}
|
||||
|
||||
public function getSemester(): string
|
||||
{
|
||||
return (string) (Configuration::getConfig('semester') ?? '');
|
||||
}
|
||||
|
||||
public function hasRosterForSemester(string $schoolYear, string $semester): bool
|
||||
{
|
||||
$year = trim((string) $schoolYear);
|
||||
$sem = trim((string) $semester);
|
||||
if ($year === '' || $sem === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = sprintf('sem:%s:%s', $year, $sem);
|
||||
if (array_key_exists($key, $this->rosterPresenceCache)) {
|
||||
return $this->rosterPresenceCache[$key];
|
||||
}
|
||||
|
||||
$cnt = DB::table('student_class')
|
||||
->where('school_year', $year)
|
||||
->where('semester', $sem)
|
||||
->count();
|
||||
|
||||
$this->rosterPresenceCache[$key] = $cnt > 0;
|
||||
return $this->rosterPresenceCache[$key];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassPreparation;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ClassPreparationInventoryService
|
||||
{
|
||||
public function buildAvailability(string $schoolYear, string $semester, bool $limitToSemester, array $allowed): array
|
||||
{
|
||||
$inventoryMap = array_fill_keys($allowed, 0);
|
||||
|
||||
$joinRows = DB::table('inventory_items as ii')
|
||||
->select('ic.name AS item_name, COALESCE(SUM(CASE WHEN ii.good_qty IS NOT NULL THEN ii.good_qty 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)
|
||||
->whereIn('ic.name', $allowed);
|
||||
|
||||
if ($limitToSemester && $semester !== '') {
|
||||
$joinRows->where('ii.semester', $semester);
|
||||
}
|
||||
|
||||
$joinRows = $joinRows->groupBy('ic.name')->get()->toArray();
|
||||
|
||||
foreach ($joinRows as $r) {
|
||||
$name = (string) ($r->item_name ?? '');
|
||||
if ($name !== '' && isset($inventoryMap[$name])) {
|
||||
$inventoryMap[$name] = max($inventoryMap[$name], (int) ($r->available ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
$nameRows = DB::table('inventory_items')
|
||||
->select('name AS item_name, COALESCE(SUM(CASE WHEN good_qty IS NOT NULL THEN good_qty WHEN `condition`=\"good\" THEN quantity ELSE 0 END),0) AS available')
|
||||
->where('type', 'classroom')
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('name', $allowed);
|
||||
|
||||
if ($limitToSemester && $semester !== '') {
|
||||
$nameRows->where('semester', $semester);
|
||||
}
|
||||
|
||||
$nameRows = $nameRows->groupBy('name')->get()->toArray();
|
||||
|
||||
foreach ($nameRows as $r) {
|
||||
$name = (string) ($r->item_name ?? '');
|
||||
if ($name !== '' && isset($inventoryMap[$name])) {
|
||||
$inventoryMap[$name] = max($inventoryMap[$name], (int) ($r->available ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
return $inventoryMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassPreparation;
|
||||
|
||||
use App\Models\ClassPreparationLog;
|
||||
|
||||
class ClassPreparationLogService
|
||||
{
|
||||
public function getLatestLog(string $classSectionId, string $schoolYear): ?ClassPreparationLog
|
||||
{
|
||||
return ClassPreparationLog::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->first();
|
||||
}
|
||||
|
||||
public function hasPrepChanged(array $new, array $old): bool
|
||||
{
|
||||
ksort($new);
|
||||
ksort($old);
|
||||
|
||||
return json_encode($new) !== json_encode($old);
|
||||
}
|
||||
|
||||
public function createLog(string $classSectionId, string $className, string $schoolYear, array $items, string $createdAt): bool
|
||||
{
|
||||
try {
|
||||
ClassPreparationLog::query()->create([
|
||||
'class_section_id' => $classSectionId,
|
||||
'class_section' => $className,
|
||||
'school_year' => $schoolYear,
|
||||
'prep_data' => json_encode($items),
|
||||
'created_at' => $createdAt,
|
||||
]);
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassPreparation;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ClassPreparationRosterService
|
||||
{
|
||||
public function getClassSectionStudentCounts(string $schoolYear, string $semester, bool $limitToSemester): array
|
||||
{
|
||||
$query = DB::table('student_class as sc')
|
||||
->select('sc.class_section_id, COUNT(DISTINCT sc.student_id) AS student_count', false)
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->where('s.is_active', 1)
|
||||
->where('sc.school_year', $schoolYear);
|
||||
|
||||
if ($limitToSemester && $semester !== '') {
|
||||
$query->where('sc.semester', $semester);
|
||||
}
|
||||
|
||||
return $query->groupBy('sc.class_section_id')->get()->toArray();
|
||||
}
|
||||
|
||||
public function getStudentCountForSection(string $schoolYear, string $semester, bool $limitToSemester, string $classSectionId): int
|
||||
{
|
||||
$query = DB::table('student_class as sc')
|
||||
->select('COUNT(DISTINCT sc.student_id) AS cnt', false)
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->where('s.is_active', 1)
|
||||
->where('sc.class_section_id', $classSectionId)
|
||||
->where('sc.school_year', $schoolYear);
|
||||
|
||||
if ($limitToSemester && $semester !== '') {
|
||||
$query->where('sc.semester', $semester);
|
||||
}
|
||||
|
||||
$row = $query->first();
|
||||
return (int) ($row->cnt ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\ClassPreparation;
|
||||
|
||||
use App\Models\ClassSection;
|
||||
|
||||
class ClassPreparationService
|
||||
{
|
||||
private ClassPreparationContextService $context;
|
||||
private ClassPreparationRosterService $roster;
|
||||
private ClassPreparationInventoryService $inventory;
|
||||
private ClassPreparationCalculatorService $calculator;
|
||||
private ClassPreparationAdjustmentService $adjustments;
|
||||
private ClassPreparationLogService $logs;
|
||||
|
||||
public function __construct(
|
||||
ClassPreparationContextService $context,
|
||||
ClassPreparationRosterService $roster,
|
||||
ClassPreparationInventoryService $inventory,
|
||||
ClassPreparationCalculatorService $calculator,
|
||||
ClassPreparationAdjustmentService $adjustments,
|
||||
ClassPreparationLogService $logs
|
||||
) {
|
||||
$this->context = $context;
|
||||
$this->roster = $roster;
|
||||
$this->inventory = $inventory;
|
||||
$this->calculator = $calculator;
|
||||
$this->adjustments = $adjustments;
|
||||
$this->logs = $logs;
|
||||
}
|
||||
|
||||
public function listPrep(?string $schoolYear = null, ?string $semester = null): array
|
||||
{
|
||||
$schoolYear = $schoolYear ?: $this->context->getSchoolYear();
|
||||
$semester = $semester ?: $this->context->getSemester();
|
||||
$allowed = $this->calculator->getAllowedCategories();
|
||||
$limitToSemester = $this->context->hasRosterForSemester($schoolYear, $semester);
|
||||
|
||||
$classSections = $this->roster->getClassSectionStudentCounts($schoolYear, $semester, $limitToSemester);
|
||||
|
||||
$inventoryMap = $this->inventory->buildAvailability($schoolYear, $semester, $limitToSemester, $allowed);
|
||||
$requiredTotals = array_fill_keys($allowed, 0);
|
||||
$results = [];
|
||||
|
||||
foreach ($classSections as $row) {
|
||||
$classSectionId = (string) $row->class_section_id;
|
||||
$studentCount = (int) $row->student_count;
|
||||
$classLevel = $this->calculator->getClassLevelBySection($classSectionId);
|
||||
$className = ClassSection::getClassSectionNameBySectionId($classSectionId);
|
||||
|
||||
$baseItems = $this->calculator->calculatePrepItems($studentCount, $classLevel, $classSectionId);
|
||||
[$baseItems, $adjMap] = $this->adjustments->applyAdjustments($baseItems, $classSectionId, $schoolYear);
|
||||
|
||||
foreach ($allowed as $cat) {
|
||||
$requiredTotals[$cat] += (int) ($baseItems[$cat] ?? 0);
|
||||
}
|
||||
|
||||
$oldSnap = $this->logs->getLatestLog($classSectionId, $schoolYear);
|
||||
$oldPrep = $oldSnap ? (array) ($oldSnap->prep_data ?? []) : [];
|
||||
$hasChanged = $this->logs->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,
|
||||
'adjustments' => $adjMap,
|
||||
];
|
||||
}
|
||||
|
||||
$shortages = [];
|
||||
foreach ($requiredTotals as $item => $reqQty) {
|
||||
$have = (int) ($inventoryMap[$item] ?? 0);
|
||||
if ($have < (int) $reqQty) {
|
||||
$shortages[$item] = (int) $reqQty - $have;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'schoolYear' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'results' => $results,
|
||||
'totals' => $requiredTotals,
|
||||
'shortages' => $shortages,
|
||||
];
|
||||
}
|
||||
|
||||
public function markPrinted(string $schoolYear, string $semester, array $classSectionIds): int
|
||||
{
|
||||
$limitToSemester = $this->context->hasRosterForSemester($schoolYear, $semester);
|
||||
$ids = array_values(array_unique(array_filter(array_map('strval', $classSectionIds))));
|
||||
$now = $this->utcNow();
|
||||
$count = 0;
|
||||
|
||||
foreach ($ids as $classSectionId) {
|
||||
$studentCount = $this->roster->getStudentCountForSection($schoolYear, $semester, $limitToSemester, $classSectionId);
|
||||
$classLevel = $this->calculator->getClassLevelBySection($classSectionId);
|
||||
$className = ClassSection::getClassSectionNameBySectionId($classSectionId) ?? $classSectionId;
|
||||
|
||||
$items = $this->calculator->calculatePrepItems($studentCount, $classLevel, $classSectionId);
|
||||
[$items] = $this->adjustments->applyAdjustments($items, $classSectionId, $schoolYear);
|
||||
|
||||
if ($this->logs->createLog($classSectionId, $className, $schoolYear, $items, $now)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function utcNow(): string
|
||||
{
|
||||
if (function_exists('utc_now')) {
|
||||
return (string) utc_now();
|
||||
}
|
||||
return now('UTC')->toDateTimeString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user