310 lines
11 KiB
PHP
310 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Inventory;
|
||
|
||
use App\Models\ClassSection;
|
||
use App\Models\InventoryItem;
|
||
use App\Models\InventoryMovement;
|
||
use App\Models\StudentClass;
|
||
use App\Models\TeacherClass;
|
||
use App\Models\User;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class InventoryTeacherDistributionService
|
||
{
|
||
public function __construct(
|
||
private InventoryContextService $context,
|
||
private InventoryMovementService $movementService
|
||
) {}
|
||
|
||
public function formData(int $userId, ?int $classSectionId, ?int $itemId): array
|
||
{
|
||
$ctx = $this->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(),
|
||
];
|
||
}
|
||
}
|