@@ -1032,6 +1032,7 @@ $routes->match(['get', 'post'], 'families/import-legacy', 'FamilyController::imp
|
|||||||
|
|
||||||
// Admin page (protect with your auth/permission)
|
// Admin page (protect with your auth/permission)
|
||||||
$routes->group('family', static function ($routes) {
|
$routes->group('family', static function ($routes) {
|
||||||
|
$routes->get('', 'View\FamilyAdminController::index');
|
||||||
$routes->get('index', 'View\FamilyAdminController::index');
|
$routes->get('index', 'View\FamilyAdminController::index');
|
||||||
$routes->get('search', 'View\FamilyAdminController::search');
|
$routes->get('search', 'View\FamilyAdminController::search');
|
||||||
$routes->get('card', 'View\FamilyAdminController::card');
|
$routes->get('card', 'View\FamilyAdminController::card');
|
||||||
|
|||||||
@@ -115,9 +115,10 @@ class InitializeRolesPermissions extends Controller
|
|||||||
|
|
||||||
// Associating permissions with roles
|
// Associating permissions with roles
|
||||||
$rolePermissions = [
|
$rolePermissions = [
|
||||||
'administrator' => ['manage_users', 'view_reports', 'manage_settings', 'access_student_records', 'communicate_parents'],
|
'administrator' => ['manage_users', 'view_reports', 'manage_settings', 'access_student_records', 'communicate_parents', 'update_curriculum'],
|
||||||
'principal' => ['manage_users', 'view_reports', 'access_student_records', 'communicate_parents'],
|
'principal' => ['manage_users', 'view_reports', 'access_student_records', 'communicate_parents', 'update_curriculum'],
|
||||||
'vice principal' => ['view_reports', 'access_student_records', 'communicate_parents'],
|
'vice principal' => ['view_reports', 'access_student_records', 'communicate_parents', 'update_curriculum'],
|
||||||
|
'admin' => ['manage_users', 'view_reports', 'manage_settings', 'access_student_records', 'communicate_parents', 'update_curriculum'],
|
||||||
'teacher' => ['access_student_records', 'communicate_parents', 'enter_grades', 'access_assignments'],
|
'teacher' => ['access_student_records', 'communicate_parents', 'enter_grades', 'access_assignments'],
|
||||||
'student' => ['view_own_records', 'access_assignments'],
|
'student' => ['view_own_records', 'access_assignments'],
|
||||||
'parent' => ['view_own_records', 'communicate_parents'],
|
'parent' => ['view_own_records', 'communicate_parents'],
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ class ParentReportCardController extends BaseController
|
|||||||
if ($schoolYear !== '') {
|
if ($schoolYear !== '') {
|
||||||
$builder->where('sc.school_year', $schoolYear);
|
$builder->where('sc.school_year', $schoolYear);
|
||||||
}
|
}
|
||||||
if ($semester !== '') {
|
|
||||||
$builder->where('sc.semester', $semester);
|
|
||||||
}
|
|
||||||
|
|
||||||
$students = $builder->get()->getResultArray();
|
$students = $builder->get()->getResultArray();
|
||||||
$studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students)));
|
$studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students)));
|
||||||
|
|||||||
@@ -570,21 +570,21 @@ class AdministratorController extends BaseController
|
|||||||
// USERS (phone col: cellphone)
|
// USERS (phone col: cellphone)
|
||||||
$uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'];
|
$uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'];
|
||||||
$uQB = $db->table('users')
|
$uQB = $db->table('users')
|
||||||
->select('id, firstname, lastname, email, cellphone, school_id, city, state, school_year, semester');
|
->select('id, firstname, lastname, email, cellphone, school_id, city, state, semester');
|
||||||
$applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']);
|
$applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']);
|
||||||
$users = $uQB->limit(150)->get()->getResultArray();
|
$users = $uQB->limit(150)->get()->getResultArray();
|
||||||
|
|
||||||
// STUDENTS (no phone column to search)
|
// STUDENTS (no phone column to search)
|
||||||
$sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'];
|
$sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'];
|
||||||
$sQB = $db->table('students')
|
$sQB = $db->table('students')
|
||||||
->select('id, parent_id, school_id, firstname, lastname, dob, gender, school_year, semester, rfid_tag');
|
->select('id, parent_id, school_id, firstname, lastname, dob, gender, semester, rfid_tag');
|
||||||
$applyMultiTokenLike($sQB, $sCols, $tokens, []);
|
$applyMultiTokenLike($sQB, $sCols, $tokens, []);
|
||||||
$students = $sQB->limit(150)->get()->getResultArray();
|
$students = $sQB->limit(150)->get()->getResultArray();
|
||||||
|
|
||||||
// PARENTS (phone col: secondparent_phone)
|
// PARENTS (phone col: secondparent_phone)
|
||||||
$pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone'];
|
$pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone'];
|
||||||
$pQB = $db->table('parents')
|
$pQB = $db->table('parents')
|
||||||
->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone, school_year, semester');
|
->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone');
|
||||||
$applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']);
|
$applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']);
|
||||||
foreach ($tokens as $t) {
|
foreach ($tokens as $t) {
|
||||||
if (ctype_digit($t)) {
|
if (ctype_digit($t)) {
|
||||||
@@ -596,14 +596,14 @@ class AdministratorController extends BaseController
|
|||||||
// STAFF (phone col: phone)
|
// STAFF (phone col: phone)
|
||||||
$stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone'];
|
$stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone'];
|
||||||
$stQB = $db->table('staff')
|
$stQB = $db->table('staff')
|
||||||
->select('id, user_id, firstname, lastname, email, phone, role_name, school_year, active_role');
|
->select('id, user_id, firstname, lastname, email, phone, role_name, active_role');
|
||||||
$applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']);
|
$applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']);
|
||||||
$staff = $stQB->limit(150)->get()->getResultArray();
|
$staff = $stQB->limit(150)->get()->getResultArray();
|
||||||
|
|
||||||
// EMERGENCY CONTACTS (phone col: cellphone)
|
// EMERGENCY CONTACTS (phone col: cellphone)
|
||||||
$ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone'];
|
$ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone'];
|
||||||
$ecQB = $db->table('emergency_contacts')
|
$ecQB = $db->table('emergency_contacts')
|
||||||
->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester');
|
->select('id, parent_id, emergency_contact_name, relation, cellphone, email');
|
||||||
$applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']);
|
$applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']);
|
||||||
$emergency = $ecQB->limit(150)->get()->getResultArray();
|
$emergency = $ecQB->limit(150)->get()->getResultArray();
|
||||||
|
|
||||||
@@ -2202,7 +2202,6 @@ class AdministratorController extends BaseController
|
|||||||
'cellphone' => $user['cellphone'],
|
'cellphone' => $user['cellphone'],
|
||||||
'gender' => $user['gender'],
|
'gender' => $user['gender'],
|
||||||
'created_at' => $user['created_at'],
|
'created_at' => $user['created_at'],
|
||||||
'school_year' => $user['school_year'],
|
|
||||||
'paid_amount' => $paidAmount,
|
'paid_amount' => $paidAmount,
|
||||||
'balance' => $balance,
|
'balance' => $balance,
|
||||||
'students' => $students,
|
'students' => $students,
|
||||||
|
|||||||
@@ -605,8 +605,6 @@ class ClassPreparationController extends BaseController
|
|||||||
{
|
{
|
||||||
$inventoryMap = array_fill_keys($allowed, 0);
|
$inventoryMap = array_fill_keys($allowed, 0);
|
||||||
|
|
||||||
$hasSchoolYear = $this->tableHasColumn('inventory_items', 'school_year');
|
|
||||||
$hasSemester = $this->tableHasColumn('inventory_items', 'semester');
|
|
||||||
$hasName = $this->tableHasColumn('inventory_items', 'name');
|
$hasName = $this->tableHasColumn('inventory_items', 'name');
|
||||||
|
|
||||||
$joinRows = $this->db->table('inventory_items ii')
|
$joinRows = $this->db->table('inventory_items ii')
|
||||||
@@ -615,12 +613,19 @@ class ClassPreparationController extends BaseController
|
|||||||
->where('ii.type', 'classroom')
|
->where('ii.type', 'classroom')
|
||||||
->whereIn('ic.name', $allowed);
|
->whereIn('ic.name', $allowed);
|
||||||
|
|
||||||
if ($hasSchoolYear && $schoolYear !== '') {
|
$periodClauses = ['im.item_id = ii.id'];
|
||||||
$joinRows->where('ii.school_year', $schoolYear);
|
if ($schoolYear !== '') {
|
||||||
|
$periodClauses[] = 'im.school_year = ' . $this->db->escape($schoolYear);
|
||||||
}
|
}
|
||||||
|
if ($semester !== '') {
|
||||||
if ($hasSemester && $semester !== '') {
|
$periodClauses[] = 'im.semester = ' . $this->db->escape($semester);
|
||||||
$joinRows->where('ii.semester', $semester);
|
}
|
||||||
|
if (count($periodClauses) > 1) {
|
||||||
|
$joinRows->where(
|
||||||
|
'EXISTS (SELECT 1 FROM inventory_movements im WHERE ' . implode(' AND ', $periodClauses) . ')',
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$joinRows = $joinRows
|
$joinRows = $joinRows
|
||||||
@@ -644,21 +649,28 @@ class ClassPreparationController extends BaseController
|
|||||||
* Only run this fallback when that column actually exists.
|
* Only run this fallback when that column actually exists.
|
||||||
*/
|
*/
|
||||||
if ($hasName) {
|
if ($hasName) {
|
||||||
$nameRows = $this->db->table('inventory_items')
|
$nameRows = $this->db->table('inventory_items ii_name')
|
||||||
->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', false)
|
->select('ii_name.name AS item_name, COALESCE(SUM(CASE WHEN ii_name.good_qty IS NOT NULL THEN ii_name.good_qty WHEN ii_name.`condition` = "good" THEN ii_name.quantity ELSE 0 END), 0) AS available', false)
|
||||||
->where('type', 'classroom')
|
->where('ii_name.type', 'classroom')
|
||||||
->whereIn('name', $allowed);
|
->whereIn('ii_name.name', $allowed);
|
||||||
|
|
||||||
if ($hasSchoolYear && $schoolYear !== '') {
|
$fallbackPeriodClauses = ['im.item_id = ii_name.id'];
|
||||||
$nameRows->where('school_year', $schoolYear);
|
if ($schoolYear !== '') {
|
||||||
|
$fallbackPeriodClauses[] = 'im.school_year = ' . $this->db->escape($schoolYear);
|
||||||
}
|
}
|
||||||
|
if ($semester !== '') {
|
||||||
if ($hasSemester && $semester !== '') {
|
$fallbackPeriodClauses[] = 'im.semester = ' . $this->db->escape($semester);
|
||||||
$nameRows->where('semester', $semester);
|
}
|
||||||
|
if (count($fallbackPeriodClauses) > 1) {
|
||||||
|
$nameRows->where(
|
||||||
|
'EXISTS (SELECT 1 FROM inventory_movements im WHERE ' . implode(' AND ', $fallbackPeriodClauses) . ')',
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$nameRows = $nameRows
|
$nameRows = $nameRows
|
||||||
->groupBy('name')
|
->groupBy('ii_name.name')
|
||||||
->get()
|
->get()
|
||||||
->getResultArray();
|
->getResultArray();
|
||||||
|
|
||||||
@@ -676,4 +688,4 @@ class ClassPreparationController extends BaseController
|
|||||||
|
|
||||||
return $inventoryMap;
|
return $inventoryMap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ class FamilyAdminController extends BaseController
|
|||||||
if ($gid > 0) $gmap[$gid] = trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? ''));
|
if ($gid > 0) $gmap[$gid] = trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? ''));
|
||||||
}
|
}
|
||||||
$ecRows = $db->table('emergency_contacts')
|
$ecRows = $db->table('emergency_contacts')
|
||||||
->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester, created_at, updated_at')
|
->select('id, parent_id, emergency_contact_name, relation, cellphone, email, created_at, updated_at')
|
||||||
->whereIn('parent_id', $parentIds)
|
->whereIn('parent_id', $parentIds)
|
||||||
->orderBy('updated_at', 'DESC')
|
->orderBy('updated_at', 'DESC')
|
||||||
->get()->getResultArray();
|
->get()->getResultArray();
|
||||||
|
|||||||
@@ -706,7 +706,6 @@ class GradingController extends Controller
|
|||||||
|
|
||||||
$existing = $this->placementLevelModel
|
$existing = $this->placementLevelModel
|
||||||
->where('student_id', $studentId)
|
->where('student_id', $studentId)
|
||||||
->where('school_year', $schoolYear)
|
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($level === null) {
|
if ($level === null) {
|
||||||
@@ -718,7 +717,6 @@ class GradingController extends Controller
|
|||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'student_id' => $studentId,
|
'student_id' => $studentId,
|
||||||
'school_year' => $schoolYear,
|
|
||||||
'level' => $level,
|
'level' => $level,
|
||||||
'updated_by' => session()->get('user_id'),
|
'updated_by' => session()->get('user_id'),
|
||||||
];
|
];
|
||||||
@@ -768,7 +766,6 @@ class GradingController extends Controller
|
|||||||
if (!empty($studentIds)) {
|
if (!empty($studentIds)) {
|
||||||
$rows = $this->placementLevelModel
|
$rows = $this->placementLevelModel
|
||||||
->whereIn('student_id', $studentIds)
|
->whereIn('student_id', $studentIds)
|
||||||
->where('school_year', $schoolYear)
|
|
||||||
->findAll();
|
->findAll();
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$levels[(int) $row['student_id']] = $row['level'] ?? null;
|
$levels[(int) $row['student_id']] = $row['level'] ?? null;
|
||||||
@@ -806,7 +803,6 @@ class GradingController extends Controller
|
|||||||
if (!empty($validIds)) {
|
if (!empty($validIds)) {
|
||||||
$rows = $this->placementLevelModel
|
$rows = $this->placementLevelModel
|
||||||
->whereIn('student_id', $validIds)
|
->whereIn('student_id', $validIds)
|
||||||
->where('school_year', $schoolYear)
|
|
||||||
->findAll();
|
->findAll();
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$existingRows[(int) $row['student_id']] = $row;
|
$existingRows[(int) $row['student_id']] = $row;
|
||||||
@@ -834,7 +830,6 @@ class GradingController extends Controller
|
|||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'student_id' => $studentId,
|
'student_id' => $studentId,
|
||||||
'school_year' => $schoolYear,
|
|
||||||
'level' => $level,
|
'level' => $level,
|
||||||
'updated_by' => $userId,
|
'updated_by' => $userId,
|
||||||
];
|
];
|
||||||
@@ -1648,7 +1643,7 @@ public function belowSixty()
|
|||||||
->join('students s', 's.id = sc.student_id', 'inner')
|
->join('students s', 's.id = sc.student_id', 'inner')
|
||||||
->join(
|
->join(
|
||||||
'placement_levels pl',
|
'placement_levels pl',
|
||||||
"pl.student_id = s.id AND pl.school_year = {$yrEsc}",
|
'pl.student_id = s.id',
|
||||||
'left'
|
'left'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -75,12 +75,7 @@ class InventoryController extends BaseController
|
|||||||
$selectedSem = $selectedSemRaw === null ? (string) $this->semester : trim((string) $selectedSemRaw);
|
$selectedSem = $selectedSemRaw === null ? (string) $this->semester : trim((string) $selectedSemRaw);
|
||||||
|
|
||||||
$builder = $this->itemModel->where('type', $type);
|
$builder = $this->itemModel->where('type', $type);
|
||||||
if (strtolower($selectedYear) !== 'all') {
|
$this->applyInventoryMovementPeriodFilter($builder, 'inventory_items', $selectedYear, $selectedSem);
|
||||||
$builder->where('school_year', $selectedYear);
|
|
||||||
}
|
|
||||||
if ($selectedSem !== '') {
|
|
||||||
$builder->where('semester', $selectedSem);
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = $builder->orderBy('name', 'ASC')->findAll();
|
$items = $builder->orderBy('name', 'ASC')->findAll();
|
||||||
$categories = $this->catModel->optionsForType($type);
|
$categories = $this->catModel->optionsForType($type);
|
||||||
@@ -474,17 +469,26 @@ class InventoryController extends BaseController
|
|||||||
// Distinct school years for a given inventory type (newest first).
|
// Distinct school years for a given inventory type (newest first).
|
||||||
private function getSchoolYearsForType(string $type): array
|
private function getSchoolYearsForType(string $type): array
|
||||||
{
|
{
|
||||||
// Pull distinct non-null school_year values for this type
|
$rows = $this->db->table('inventory_movements m')
|
||||||
$years = $this->itemModel
|
->distinct()
|
||||||
->select('school_year')
|
->select('m.school_year')
|
||||||
->where('type', $type)
|
->join('inventory_items i', 'i.id = m.item_id', 'inner')
|
||||||
->where('school_year IS NOT NULL', null, false)
|
->where('m.school_year IS NOT NULL', null, false)
|
||||||
->groupBy('school_year')
|
->where("TRIM(m.school_year) <>", '')
|
||||||
->orderBy('school_year', 'DESC')
|
->orderBy('m.school_year', 'DESC');
|
||||||
->findColumn('school_year') ?? [];
|
|
||||||
|
if (strtolower($type) !== 'all') {
|
||||||
|
$rows->where('i.type', $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $rows->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
// Normalize & de-dup
|
// Normalize & de-dup
|
||||||
$years = array_values(array_unique(array_filter(array_map('trim', $years))));
|
$years = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn(array $row): string => trim((string)($row['school_year'] ?? '')),
|
||||||
|
$rows
|
||||||
|
))));
|
||||||
|
|
||||||
// Ensure the current year (from config) appears as an option, even if no rows yet
|
// Ensure the current year (from config) appears as an option, even if no rows yet
|
||||||
$currentYear = $this->schoolYear;
|
$currentYear = $this->schoolYear;
|
||||||
@@ -496,6 +500,31 @@ class InventoryController extends BaseController
|
|||||||
return $years;
|
return $years;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function applyInventoryMovementPeriodFilter($builder, string $itemAlias, ?string $schoolYear, ?string $semester = null): void
|
||||||
|
{
|
||||||
|
$schoolYear = trim((string) $schoolYear);
|
||||||
|
$semester = trim((string) $semester);
|
||||||
|
$clauses = ["m.item_id = {$itemAlias}.id"];
|
||||||
|
|
||||||
|
if ($schoolYear !== '' && strtolower($schoolYear) !== 'all') {
|
||||||
|
$clauses[] = 'm.school_year = ' . $this->db->escape($schoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($semester !== '') {
|
||||||
|
$clauses[] = 'm.semester = ' . $this->db->escape($semester);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($clauses) <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$builder->where(
|
||||||
|
'EXISTS (SELECT 1 FROM inventory_movements m WHERE ' . implode(' AND ', $clauses) . ')',
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function recordMovement(
|
private function recordMovement(
|
||||||
int $itemId,
|
int $itemId,
|
||||||
int $qtyChange,
|
int $qtyChange,
|
||||||
@@ -1095,7 +1124,7 @@ class InventoryController extends BaseController
|
|||||||
|
|
||||||
// Filter by school year unless "all"
|
// Filter by school year unless "all"
|
||||||
if (!$isAllYears) {
|
if (!$isAllYears) {
|
||||||
$qbItems->where('i.school_year', $selectedYear);
|
$this->applyInventoryMovementPeriodFilter($qbItems, 'i', $selectedYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: filter by type if provided
|
// Optional: filter by type if provided
|
||||||
@@ -1123,7 +1152,7 @@ class InventoryController extends BaseController
|
|||||||
return view('inventory/summary', [
|
return view('inventory/summary', [
|
||||||
'selectedYear' => $selectedYear,
|
'selectedYear' => $selectedYear,
|
||||||
'currentYear' => $this->schoolYear,
|
'currentYear' => $this->schoolYear,
|
||||||
'schoolYears' => $this->getSchoolYearsForType('book') ?: [$this->schoolYear, 'All'],
|
'schoolYears' => $this->getSchoolYearsForType($selectedType) ?: [$this->schoolYear, 'All'],
|
||||||
'rows' => $rows,
|
'rows' => $rows,
|
||||||
'totals' => $totals,
|
'totals' => $totals,
|
||||||
// optionally pass the selected type if your view supports it
|
// optionally pass the selected type if your view supports it
|
||||||
@@ -1202,7 +1231,7 @@ class InventoryController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------- Year dropdown options --------
|
// -------- Year dropdown options --------
|
||||||
$schoolYears = $this->getSchoolYearsForType('book') ?: [$this->schoolYear, 'All'];
|
$schoolYears = $this->getSchoolYearsForType($selectedType) ?: [$this->schoolYear, 'All'];
|
||||||
|
|
||||||
return view('inventory/summary', [
|
return view('inventory/summary', [
|
||||||
'selectedYear' => $selectedYear,
|
'selectedYear' => $selectedYear,
|
||||||
|
|||||||
@@ -1487,8 +1487,6 @@ $existing = $this->studentModel
|
|||||||
'cellphone' => $phone,
|
'cellphone' => $phone,
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'relation' => $relation,
|
'relation' => $relation,
|
||||||
'semester' => $semester,
|
|
||||||
'school_year' => $schoolYear,
|
|
||||||
'updated_at' => utc_now(),
|
'updated_at' => utc_now(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1568,8 +1566,6 @@ $existing = $this->studentModel
|
|||||||
'cellphone' => $phone,
|
'cellphone' => $phone,
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'relation' => $relation,
|
'relation' => $relation,
|
||||||
'semester' => $semester,
|
|
||||||
'school_year' => $schoolYear,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class GrantSubjectCurriculumPermission extends Migration
|
||||||
|
{
|
||||||
|
private string $permissionName = 'update_curriculum';
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
! $this->db->tableExists('roles')
|
||||||
|
|| ! $this->db->tableExists('permissions')
|
||||||
|
|| ! $this->db->tableExists('role_permissions')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$permission = $this->db->table('permissions')
|
||||||
|
->where('name', $this->permissionName)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if ($permission === null) {
|
||||||
|
$insert = [
|
||||||
|
'name' => $this->permissionName,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->db->fieldExists('description', 'permissions')) {
|
||||||
|
$insert['description'] = 'Manage subject curriculum entries';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->table('permissions')->insert($insert);
|
||||||
|
$permissionId = (int) $this->db->insertID();
|
||||||
|
} else {
|
||||||
|
$permissionId = (int) $permission['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($permissionId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleRows = $this->db->table('roles')
|
||||||
|
->select('id, name')
|
||||||
|
->whereIn('name', [
|
||||||
|
'administrator',
|
||||||
|
'admin',
|
||||||
|
'principal',
|
||||||
|
'vice principal',
|
||||||
|
'vice_principal',
|
||||||
|
'administrative staff',
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
foreach ($roleRows as $role) {
|
||||||
|
$roleId = (int) ($role['id'] ?? 0);
|
||||||
|
if ($roleId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->db->table('role_permissions')
|
||||||
|
->where('role_id', $roleId)
|
||||||
|
->where('permission_id', $permissionId)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
$permissionData = [
|
||||||
|
'can_create' => 1,
|
||||||
|
'can_read' => 1,
|
||||||
|
'can_update' => 1,
|
||||||
|
'can_delete' => 1,
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->db->fieldExists('can_manage', 'role_permissions')) {
|
||||||
|
$permissionData['can_manage'] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existing === null) {
|
||||||
|
$permissionData['role_id'] = $roleId;
|
||||||
|
$permissionData['permission_id'] = $permissionId;
|
||||||
|
$permissionData['created_at'] = $now;
|
||||||
|
|
||||||
|
$this->db->table('role_permissions')->insert($permissionData);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->table('role_permissions')
|
||||||
|
->where('id', (int) $existing['id'])
|
||||||
|
->update($permissionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
! $this->db->tableExists('roles')
|
||||||
|
|| ! $this->db->tableExists('permissions')
|
||||||
|
|| ! $this->db->tableExists('role_permissions')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$permission = $this->db->table('permissions')
|
||||||
|
->select('id')
|
||||||
|
->where('name', $this->permissionName)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if ($permission === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleRows = $this->db->table('roles')
|
||||||
|
->select('id')
|
||||||
|
->whereIn('name', [
|
||||||
|
'administrator',
|
||||||
|
'admin',
|
||||||
|
'principal',
|
||||||
|
'vice principal',
|
||||||
|
'vice_principal',
|
||||||
|
'administrative staff',
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$roleIds = array_map(static fn(array $role): int => (int) ($role['id'] ?? 0), $roleRows);
|
||||||
|
$roleIds = array_values(array_filter($roleIds, static fn(int $roleId): bool => $roleId > 0));
|
||||||
|
|
||||||
|
if ($roleIds === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->table('role_permissions')
|
||||||
|
->where('permission_id', (int) $permission['id'])
|
||||||
|
->whereIn('role_id', $roleIds)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -305,6 +305,23 @@
|
|||||||
|
|
||||||
const normalize = s => (s||'').toString().toLowerCase();
|
const normalize = s => (s||'').toString().toLowerCase();
|
||||||
const debounce = (fn, wait=200) => { let t; return (...args)=>{ clearTimeout(t); t=setTimeout(()=>fn(...args), wait); }; };
|
const debounce = (fn, wait=200) => { let t; return (...args)=>{ clearTimeout(t); t=setTimeout(()=>fn(...args), wait); }; };
|
||||||
|
const openFamilyResult = (type, id) => {
|
||||||
|
const params = {};
|
||||||
|
if (type === 'student') {
|
||||||
|
params.student_id = id;
|
||||||
|
} else if (type === 'guardian') {
|
||||||
|
params.guardian_id = id;
|
||||||
|
}
|
||||||
|
if (Object.keys(params).length && typeof window.openFamilyCard === 'function') {
|
||||||
|
window.openFamilyCard(params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'student') {
|
||||||
|
window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(id);
|
||||||
|
} else if (type === 'guardian') {
|
||||||
|
window.location.href = '<?= site_url('family') ?>' + '?guardian_id=' + encodeURIComponent(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Preloaded datasets for suggestions (students + guardians)
|
// Preloaded datasets for suggestions (students + guardians)
|
||||||
const STATIC_ITEMS = [
|
const STATIC_ITEMS = [
|
||||||
@@ -368,12 +385,7 @@
|
|||||||
btn.className = 'list-group-item list-group-item-action';
|
btn.className = 'list-group-item list-group-item-action';
|
||||||
btn.innerHTML = `<div class="d-flex justify-content-between"><span>${(it.label||'').replace(/</g,'<')}</span><span class="text-muted small">${(it.sub||'').replace(/</g,'<')}</span></div>`;
|
btn.innerHTML = `<div class="d-flex justify-content-between"><span>${(it.label||'').replace(/</g,'<')}</span><span class="text-muted small">${(it.sub||'').replace(/</g,'<')}</span></div>`;
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
// Redirect to show only the selected family
|
openFamilyResult(it.type, it.id);
|
||||||
if (it.type === 'student') {
|
|
||||||
window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(it.id);
|
|
||||||
} else if (it.type === 'guardian') {
|
|
||||||
window.location.href = '<?= site_url('family') ?>' + '?guardian_id=' + encodeURIComponent(it.id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
suggest.appendChild(btn);
|
suggest.appendChild(btn);
|
||||||
});
|
});
|
||||||
@@ -455,7 +467,7 @@
|
|||||||
}
|
}
|
||||||
if (best) {
|
if (best) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = '<?= site_url('family') ?>' + '?student_id=' + encodeURIComponent(best.value);
|
openFamilyResult('student', best.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Hide suggestions on outside click
|
// Hide suggestions on outside click
|
||||||
|
|||||||
@@ -153,6 +153,13 @@
|
|||||||
/* Prevent horizontal scrollbars from stray overflows */
|
/* Prevent horizontal scrollbars from stray overflows */
|
||||||
html, body { overflow-x: hidden; }
|
html, body { overflow-x: hidden; }
|
||||||
|
|
||||||
|
#familyCardModal .family-card-modal-dialog {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
#familyCardModal .modal-content {
|
||||||
|
max-height: calc(100vh - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -237,7 +244,7 @@ html, body { overflow-x: hidden; }
|
|||||||
|
|
||||||
<!-- Family Card Modal -->
|
<!-- Family Card Modal -->
|
||||||
<div class="modal fade" id="familyCardModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="familyCardModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
<div class="modal-dialog modal-xl modal-dialog-scrollable family-card-modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Family</h5>
|
<h5 class="modal-title">Family</h5>
|
||||||
@@ -312,33 +319,60 @@ html, body { overflow-x: hidden; }
|
|||||||
}
|
}
|
||||||
window.openFamilyCard = loadFamilyCard;
|
window.openFamilyCard = loadFamilyCard;
|
||||||
|
|
||||||
document.addEventListener('click', function(e){
|
function familyParamsFromTarget(target) {
|
||||||
const link = e.target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id]');
|
const link = target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id], a[href]');
|
||||||
if (!link) return;
|
if (!link) return null;
|
||||||
e.preventDefault();
|
let sid = link.getAttribute('data-family-student-id');
|
||||||
const sid = link.getAttribute('data-family-student-id');
|
let gid = link.getAttribute('data-family-guardian-id');
|
||||||
const gid = link.getAttribute('data-family-guardian-id');
|
let fid = link.getAttribute('data-family-id');
|
||||||
const fid = link.getAttribute('data-family-id');
|
if (!sid && !gid && !fid && link.matches('a[href]')) {
|
||||||
|
try {
|
||||||
|
const url = new URL(link.getAttribute('href'), window.location.origin);
|
||||||
|
const familyPath = new URL('<?= site_url('family') ?>', window.location.origin).pathname.replace(/\/+$/, '');
|
||||||
|
const linkPath = url.pathname.replace(/\/+$/, '');
|
||||||
|
if (url.origin !== window.location.origin || linkPath !== familyPath) return null;
|
||||||
|
sid = url.searchParams.get('student_id');
|
||||||
|
gid = url.searchParams.get('guardian_id');
|
||||||
|
fid = url.searchParams.get('family_id');
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const params = {};
|
const params = {};
|
||||||
if (fid) params.family_id = fid;
|
if (fid) params.family_id = fid;
|
||||||
else if (sid) params.student_id = sid;
|
else if (sid) params.student_id = sid;
|
||||||
else if (gid) params.guardian_id = gid;
|
else if (gid) params.guardian_id = gid;
|
||||||
if (Object.keys(params).length) loadFamilyCard(params);
|
return Object.keys(params).length ? params : null;
|
||||||
});
|
}
|
||||||
|
|
||||||
document.addEventListener('click', function(e){
|
function handleFamilyCardClick(e) {
|
||||||
const link = e.target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id]');
|
const params = familyParamsFromTarget(e.target);
|
||||||
if (!link) return;
|
if (!params) return;
|
||||||
|
if (e.familyCardHandled) return;
|
||||||
|
e.familyCardHandled = true;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const sid = link.getAttribute('data-family-student-id');
|
loadFamilyCard(params);
|
||||||
const gid = link.getAttribute('data-family-guardian-id');
|
}
|
||||||
const fid = link.getAttribute('data-family-id');
|
|
||||||
const params = {};
|
document.addEventListener('click', handleFamilyCardClick);
|
||||||
if (fid) params.family_id = fid;
|
document.addEventListener('click', handleFamilyCardClick, true);
|
||||||
else if (sid) params.student_id = sid;
|
|
||||||
else if (gid) params.guardian_id = gid;
|
document.addEventListener('DOMContentLoaded', function(){
|
||||||
if (Object.keys(params).length) loadFamilyCard(params);
|
try {
|
||||||
}, true);
|
const current = new URL(window.location.href);
|
||||||
|
const familyPath = new URL('<?= site_url('family') ?>', window.location.origin).pathname.replace(/\/+$/, '');
|
||||||
|
const currentPath = current.pathname.replace(/\/+$/, '');
|
||||||
|
if (currentPath !== familyPath) return;
|
||||||
|
const sid = current.searchParams.get('student_id');
|
||||||
|
const gid = current.searchParams.get('guardian_id');
|
||||||
|
const fid = current.searchParams.get('family_id');
|
||||||
|
const params = {};
|
||||||
|
if (fid) params.family_id = fid;
|
||||||
|
else if (sid) params.student_id = sid;
|
||||||
|
else if (gid) params.guardian_id = gid;
|
||||||
|
if (Object.keys(params).length) loadFamilyCard(params);
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,12 @@
|
|||||||
}
|
}
|
||||||
.card-header { background-color: #f0f7ff; }
|
.card-header { background-color: #f0f7ff; }
|
||||||
.form-text { color: var(--mgmt-muted); }
|
.form-text { color: var(--mgmt-muted); }
|
||||||
|
#familyCardModal .family-card-modal-dialog {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
#familyCardModal .modal-content {
|
||||||
|
max-height: calc(100vh - 2rem);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<?= $this->renderSection('styles') ?>
|
<?= $this->renderSection('styles') ?>
|
||||||
</head>
|
</head>
|
||||||
@@ -295,7 +301,7 @@
|
|||||||
|
|
||||||
<!-- Family Card Modal -->
|
<!-- Family Card Modal -->
|
||||||
<div class="modal fade" id="familyCardModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="familyCardModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
<div class="modal-dialog modal-xl modal-dialog-scrollable family-card-modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Family</h5>
|
<h5 class="modal-title">Family</h5>
|
||||||
@@ -594,36 +600,63 @@
|
|||||||
// Expose for programmatic usage
|
// Expose for programmatic usage
|
||||||
window.openFamilyCard = loadFamilyCard;
|
window.openFamilyCard = loadFamilyCard;
|
||||||
|
|
||||||
// Bubble-phase handler (general)
|
function familyParamsFromTarget(target) {
|
||||||
document.addEventListener('click', function(e){
|
const link = target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id], a[href]');
|
||||||
const link = e.target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id]');
|
if (!link) return null;
|
||||||
if (!link) return;
|
let sid = link.getAttribute('data-family-student-id');
|
||||||
e.preventDefault();
|
let gid = link.getAttribute('data-family-guardian-id');
|
||||||
const sid = link.getAttribute('data-family-student-id');
|
let fid = link.getAttribute('data-family-id');
|
||||||
const gid = link.getAttribute('data-family-guardian-id');
|
if (!sid && !gid && !fid && link.matches('a[href]')) {
|
||||||
const fid = link.getAttribute('data-family-id');
|
try {
|
||||||
|
const url = new URL(link.getAttribute('href'), window.location.origin);
|
||||||
|
const familyPath = new URL('<?= site_url('family') ?>', window.location.origin).pathname.replace(/\/+$/, '');
|
||||||
|
const linkPath = url.pathname.replace(/\/+$/, '');
|
||||||
|
if (url.origin !== window.location.origin || linkPath !== familyPath) return null;
|
||||||
|
sid = url.searchParams.get('student_id');
|
||||||
|
gid = url.searchParams.get('guardian_id');
|
||||||
|
fid = url.searchParams.get('family_id');
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
const params = {};
|
const params = {};
|
||||||
if (fid) params.family_id = fid;
|
if (fid) params.family_id = fid;
|
||||||
else if (sid) params.student_id = sid;
|
else if (sid) params.student_id = sid;
|
||||||
else if (gid) params.guardian_id = gid;
|
else if (gid) params.guardian_id = gid;
|
||||||
if (Object.keys(params).length) loadFamilyCard(params);
|
return Object.keys(params).length ? params : null;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function handleFamilyCardClick(e) {
|
||||||
|
const params = familyParamsFromTarget(e.target);
|
||||||
|
if (!params) return;
|
||||||
|
if (e.familyCardHandled) return;
|
||||||
|
e.familyCardHandled = true;
|
||||||
|
e.preventDefault();
|
||||||
|
loadFamilyCard(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bubble-phase handler (general)
|
||||||
|
document.addEventListener('click', handleFamilyCardClick);
|
||||||
|
|
||||||
// Capture-phase safety net so components that stop propagation (e.g., DataTables) don't block us
|
// Capture-phase safety net so components that stop propagation (e.g., DataTables) don't block us
|
||||||
document.addEventListener('click', function(e){
|
document.addEventListener('click', handleFamilyCardClick, true);
|
||||||
const link = e.target.closest('[data-family-student-id], [data-family-guardian-id], [data-family-id]');
|
|
||||||
if (!link) return;
|
document.addEventListener('DOMContentLoaded', function(){
|
||||||
// If already prevented by other handler, still proceed
|
try {
|
||||||
e.preventDefault();
|
const current = new URL(window.location.href);
|
||||||
const sid = link.getAttribute('data-family-student-id');
|
const familyPath = new URL('<?= site_url('family') ?>', window.location.origin).pathname.replace(/\/+$/, '');
|
||||||
const gid = link.getAttribute('data-family-guardian-id');
|
const currentPath = current.pathname.replace(/\/+$/, '');
|
||||||
const fid = link.getAttribute('data-family-id');
|
if (currentPath !== familyPath) return;
|
||||||
const params = {};
|
const sid = current.searchParams.get('student_id');
|
||||||
if (fid) params.family_id = fid;
|
const gid = current.searchParams.get('guardian_id');
|
||||||
else if (sid) params.student_id = sid;
|
const fid = current.searchParams.get('family_id');
|
||||||
else if (gid) params.guardian_id = gid;
|
const params = {};
|
||||||
if (Object.keys(params).length) loadFamilyCard(params);
|
if (fid) params.family_id = fid;
|
||||||
}, true);
|
else if (sid) params.student_id = sid;
|
||||||
|
else if (gid) params.guardian_id = gid;
|
||||||
|
if (Object.keys(params).length) loadFamilyCard(params);
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\App\Controllers\View;
|
||||||
|
|
||||||
|
use App\Controllers\View\InventoryController;
|
||||||
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionProperty;
|
||||||
|
|
||||||
|
class NormalizedInventoryTestController extends InventoryController
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Avoid opening real model/database connections for these regression tests.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InventoryFilterBuilderSpy
|
||||||
|
{
|
||||||
|
public array $whereCalls = [];
|
||||||
|
|
||||||
|
public function where($key, $value = null, ?bool $escape = null): self
|
||||||
|
{
|
||||||
|
$this->whereCalls[] = [$key, $value, $escape];
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InventoryFilterDbStub
|
||||||
|
{
|
||||||
|
public function escape($value): string
|
||||||
|
{
|
||||||
|
return "'" . str_replace("'", "''", (string) $value) . "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InventorySchemaNormalizationTest extends CIUnitTestCase
|
||||||
|
{
|
||||||
|
public function testAffectedRoutesAreRegistered(): void
|
||||||
|
{
|
||||||
|
$routes = file_get_contents(ROOTPATH . 'app/Config/Routes.php');
|
||||||
|
|
||||||
|
$this->assertStringContainsString("\$routes->get('card', 'View\\FamilyAdminController::card')", $routes);
|
||||||
|
$this->assertStringContainsString("\$routes->get('summary-all', 'View\\InventoryController::summaryAll')", $routes);
|
||||||
|
$this->assertStringContainsString("\$routes->get('/', 'View\\InventoryController::index')", $routes);
|
||||||
|
$this->assertStringContainsString("\$routes->get('(classroom|book|office|kitchen)', 'View\\InventoryController::index/\$1')", $routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInventoryPeriodFilterUsesMovementExistsWithoutJoiningDuplicates(): void
|
||||||
|
{
|
||||||
|
$controller = new NormalizedInventoryTestController();
|
||||||
|
$dbProperty = new ReflectionProperty(InventoryController::class, 'db');
|
||||||
|
$dbProperty->setAccessible(true);
|
||||||
|
$dbProperty->setValue($controller, new InventoryFilterDbStub());
|
||||||
|
|
||||||
|
$builder = new InventoryFilterBuilderSpy();
|
||||||
|
$method = (new ReflectionClass(InventoryController::class))->getMethod('applyInventoryMovementPeriodFilter');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$method->invoke($controller, $builder, 'i', '2025-2026', 'Fall');
|
||||||
|
|
||||||
|
$this->assertCount(1, $builder->whereCalls);
|
||||||
|
[$sql, $value, $escape] = $builder->whereCalls[0];
|
||||||
|
$this->assertNull($value);
|
||||||
|
$this->assertFalse($escape);
|
||||||
|
$this->assertStringContainsString('EXISTS (SELECT 1 FROM inventory_movements m', $sql);
|
||||||
|
$this->assertStringContainsString('m.item_id = i.id', $sql);
|
||||||
|
$this->assertStringContainsString("m.school_year = '2025-2026'", $sql);
|
||||||
|
$this->assertStringContainsString("m.semester = 'Fall'", $sql);
|
||||||
|
$this->assertStringNotContainsString('inventory_items.school_year', $sql);
|
||||||
|
$this->assertStringNotContainsString('i.school_year', $sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInventoryAllYearsWithoutSemesterDoesNotFilterItems(): void
|
||||||
|
{
|
||||||
|
$controller = new NormalizedInventoryTestController();
|
||||||
|
$dbProperty = new ReflectionProperty(InventoryController::class, 'db');
|
||||||
|
$dbProperty->setAccessible(true);
|
||||||
|
$dbProperty->setValue($controller, new InventoryFilterDbStub());
|
||||||
|
|
||||||
|
$builder = new InventoryFilterBuilderSpy();
|
||||||
|
$method = (new ReflectionClass(InventoryController::class))->getMethod('applyInventoryMovementPeriodFilter');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$method->invoke($controller, $builder, 'i', 'All', '');
|
||||||
|
|
||||||
|
$this->assertSame([], $builder->whereCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPatchedFilesDoNotReferenceRemovedNormalizedColumns(): void
|
||||||
|
{
|
||||||
|
$familyAdmin = file_get_contents(ROOTPATH . 'app/Controllers/View/FamilyAdminController.php');
|
||||||
|
$parent = file_get_contents(ROOTPATH . 'app/Controllers/View/ParentController.php');
|
||||||
|
$inventory = file_get_contents(ROOTPATH . 'app/Controllers/View/InventoryController.php');
|
||||||
|
$classPreparation = file_get_contents(ROOTPATH . 'app/Controllers/View/ClassPreparationController.php');
|
||||||
|
|
||||||
|
$this->assertStringNotContainsString('emergency_contact_name, relation, cellphone, email, school_year, semester', $familyAdmin);
|
||||||
|
$this->assertStringNotContainsString("'school_year' => \$schoolYear", $parent);
|
||||||
|
$this->assertStringNotContainsString("'semester' => \$semester", $parent);
|
||||||
|
$this->assertStringNotContainsString("->where('i.school_year'", $inventory);
|
||||||
|
$this->assertStringNotContainsString("->where('ii.school_year'", $classPreparation);
|
||||||
|
$this->assertStringNotContainsString("->where('ii.semester'", $classPreparation);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user