diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 6bd3141..6a3a8f5 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1032,6 +1032,7 @@ $routes->match(['get', 'post'], 'families/import-legacy', 'FamilyController::imp // Admin page (protect with your auth/permission) $routes->group('family', static function ($routes) { + $routes->get('', 'View\FamilyAdminController::index'); $routes->get('index', 'View\FamilyAdminController::index'); $routes->get('search', 'View\FamilyAdminController::search'); $routes->get('card', 'View\FamilyAdminController::card'); diff --git a/app/Controllers/InitializeRolesPermissions.php b/app/Controllers/InitializeRolesPermissions.php index fbf6a00..bad2a5e 100644 --- a/app/Controllers/InitializeRolesPermissions.php +++ b/app/Controllers/InitializeRolesPermissions.php @@ -115,9 +115,10 @@ class InitializeRolesPermissions extends Controller // Associating permissions with roles $rolePermissions = [ - 'administrator' => ['manage_users', 'view_reports', 'manage_settings', 'access_student_records', 'communicate_parents'], - 'principal' => ['manage_users', 'view_reports', 'access_student_records', 'communicate_parents'], - 'vice principal' => ['view_reports', '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', 'update_curriculum'], + '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'], 'student' => ['view_own_records', 'access_assignments'], 'parent' => ['view_own_records', 'communicate_parents'], diff --git a/app/Controllers/ParentReportCardController.php b/app/Controllers/ParentReportCardController.php index 8206712..ff3176d 100644 --- a/app/Controllers/ParentReportCardController.php +++ b/app/Controllers/ParentReportCardController.php @@ -45,9 +45,6 @@ class ParentReportCardController extends BaseController if ($schoolYear !== '') { $builder->where('sc.school_year', $schoolYear); } - if ($semester !== '') { - $builder->where('sc.semester', $semester); - } $students = $builder->get()->getResultArray(); $studentIds = array_values(array_filter(array_map(static fn ($s) => (int) ($s['id'] ?? 0), $students))); diff --git a/app/Controllers/View/AdministratorController.php b/app/Controllers/View/AdministratorController.php index 434ef9e..b7ae42e 100644 --- a/app/Controllers/View/AdministratorController.php +++ b/app/Controllers/View/AdministratorController.php @@ -570,21 +570,21 @@ class AdministratorController extends BaseController // USERS (phone col: cellphone) $uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state']; $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']); $users = $uQB->limit(150)->get()->getResultArray(); // STUDENTS (no phone column to search) $sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender']; $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, []); $students = $sQB->limit(150)->get()->getResultArray(); // PARENTS (phone col: secondparent_phone) $pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone']; $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']); foreach ($tokens as $t) { if (ctype_digit($t)) { @@ -596,14 +596,14 @@ class AdministratorController extends BaseController // STAFF (phone col: phone) $stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone']; $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']); $staff = $stQB->limit(150)->get()->getResultArray(); // EMERGENCY CONTACTS (phone col: cellphone) $ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone']; $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']); $emergency = $ecQB->limit(150)->get()->getResultArray(); @@ -2202,7 +2202,6 @@ class AdministratorController extends BaseController 'cellphone' => $user['cellphone'], 'gender' => $user['gender'], 'created_at' => $user['created_at'], - 'school_year' => $user['school_year'], 'paid_amount' => $paidAmount, 'balance' => $balance, 'students' => $students, diff --git a/app/Controllers/View/ClassPreparationController.php b/app/Controllers/View/ClassPreparationController.php index 8e14480..c8eba8a 100644 --- a/app/Controllers/View/ClassPreparationController.php +++ b/app/Controllers/View/ClassPreparationController.php @@ -605,8 +605,6 @@ class ClassPreparationController extends BaseController { $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'); $joinRows = $this->db->table('inventory_items ii') @@ -615,12 +613,19 @@ class ClassPreparationController extends BaseController ->where('ii.type', 'classroom') ->whereIn('ic.name', $allowed); - if ($hasSchoolYear && $schoolYear !== '') { - $joinRows->where('ii.school_year', $schoolYear); + $periodClauses = ['im.item_id = ii.id']; + if ($schoolYear !== '') { + $periodClauses[] = 'im.school_year = ' . $this->db->escape($schoolYear); } - - if ($hasSemester && $semester !== '') { - $joinRows->where('ii.semester', $semester); + if ($semester !== '') { + $periodClauses[] = 'im.semester = ' . $this->db->escape($semester); + } + if (count($periodClauses) > 1) { + $joinRows->where( + 'EXISTS (SELECT 1 FROM inventory_movements im WHERE ' . implode(' AND ', $periodClauses) . ')', + null, + false + ); } $joinRows = $joinRows @@ -644,21 +649,28 @@ class ClassPreparationController extends BaseController * Only run this fallback when that column actually exists. */ if ($hasName) { - $nameRows = $this->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', false) - ->where('type', 'classroom') - ->whereIn('name', $allowed); + $nameRows = $this->db->table('inventory_items ii_name') + ->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('ii_name.type', 'classroom') + ->whereIn('ii_name.name', $allowed); - if ($hasSchoolYear && $schoolYear !== '') { - $nameRows->where('school_year', $schoolYear); + $fallbackPeriodClauses = ['im.item_id = ii_name.id']; + if ($schoolYear !== '') { + $fallbackPeriodClauses[] = 'im.school_year = ' . $this->db->escape($schoolYear); } - - if ($hasSemester && $semester !== '') { - $nameRows->where('semester', $semester); + if ($semester !== '') { + $fallbackPeriodClauses[] = 'im.semester = ' . $this->db->escape($semester); + } + if (count($fallbackPeriodClauses) > 1) { + $nameRows->where( + 'EXISTS (SELECT 1 FROM inventory_movements im WHERE ' . implode(' AND ', $fallbackPeriodClauses) . ')', + null, + false + ); } $nameRows = $nameRows - ->groupBy('name') + ->groupBy('ii_name.name') ->get() ->getResultArray(); @@ -676,4 +688,4 @@ class ClassPreparationController extends BaseController return $inventoryMap; } -} \ No newline at end of file +} diff --git a/app/Controllers/View/FamilyAdminController.php b/app/Controllers/View/FamilyAdminController.php index 4ba5065..c230948 100644 --- a/app/Controllers/View/FamilyAdminController.php +++ b/app/Controllers/View/FamilyAdminController.php @@ -361,7 +361,7 @@ class FamilyAdminController extends BaseController if ($gid > 0) $gmap[$gid] = trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? '')); } $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) ->orderBy('updated_at', 'DESC') ->get()->getResultArray(); diff --git a/app/Controllers/View/GradingController.php b/app/Controllers/View/GradingController.php index e753c3a..1f271b4 100644 --- a/app/Controllers/View/GradingController.php +++ b/app/Controllers/View/GradingController.php @@ -706,7 +706,6 @@ class GradingController extends Controller $existing = $this->placementLevelModel ->where('student_id', $studentId) - ->where('school_year', $schoolYear) ->first(); if ($level === null) { @@ -718,7 +717,6 @@ class GradingController extends Controller $payload = [ 'student_id' => $studentId, - 'school_year' => $schoolYear, 'level' => $level, 'updated_by' => session()->get('user_id'), ]; @@ -768,7 +766,6 @@ class GradingController extends Controller if (!empty($studentIds)) { $rows = $this->placementLevelModel ->whereIn('student_id', $studentIds) - ->where('school_year', $schoolYear) ->findAll(); foreach ($rows as $row) { $levels[(int) $row['student_id']] = $row['level'] ?? null; @@ -806,7 +803,6 @@ class GradingController extends Controller if (!empty($validIds)) { $rows = $this->placementLevelModel ->whereIn('student_id', $validIds) - ->where('school_year', $schoolYear) ->findAll(); foreach ($rows as $row) { $existingRows[(int) $row['student_id']] = $row; @@ -834,7 +830,6 @@ class GradingController extends Controller $payload = [ 'student_id' => $studentId, - 'school_year' => $schoolYear, 'level' => $level, 'updated_by' => $userId, ]; @@ -1648,7 +1643,7 @@ public function belowSixty() ->join('students s', 's.id = sc.student_id', 'inner') ->join( 'placement_levels pl', - "pl.student_id = s.id AND pl.school_year = {$yrEsc}", + 'pl.student_id = s.id', 'left' ) diff --git a/app/Controllers/View/InventoryController.php b/app/Controllers/View/InventoryController.php index 5540ea6..e7fef8f 100644 --- a/app/Controllers/View/InventoryController.php +++ b/app/Controllers/View/InventoryController.php @@ -75,12 +75,7 @@ class InventoryController extends BaseController $selectedSem = $selectedSemRaw === null ? (string) $this->semester : trim((string) $selectedSemRaw); $builder = $this->itemModel->where('type', $type); - if (strtolower($selectedYear) !== 'all') { - $builder->where('school_year', $selectedYear); - } - if ($selectedSem !== '') { - $builder->where('semester', $selectedSem); - } + $this->applyInventoryMovementPeriodFilter($builder, 'inventory_items', $selectedYear, $selectedSem); $items = $builder->orderBy('name', 'ASC')->findAll(); $categories = $this->catModel->optionsForType($type); @@ -474,17 +469,26 @@ class InventoryController extends BaseController // Distinct school years for a given inventory type (newest first). private function getSchoolYearsForType(string $type): array { - // Pull distinct non-null school_year values for this type - $years = $this->itemModel - ->select('school_year') - ->where('type', $type) - ->where('school_year IS NOT NULL', null, false) - ->groupBy('school_year') - ->orderBy('school_year', 'DESC') - ->findColumn('school_year') ?? []; + $rows = $this->db->table('inventory_movements m') + ->distinct() + ->select('m.school_year') + ->join('inventory_items i', 'i.id = m.item_id', 'inner') + ->where('m.school_year IS NOT NULL', null, false) + ->where("TRIM(m.school_year) <>", '') + ->orderBy('m.school_year', 'DESC'); + + if (strtolower($type) !== 'all') { + $rows->where('i.type', $type); + } + + $rows = $rows->get() + ->getResultArray(); // 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 $currentYear = $this->schoolYear; @@ -496,6 +500,31 @@ class InventoryController extends BaseController 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( int $itemId, int $qtyChange, @@ -1095,7 +1124,7 @@ class InventoryController extends BaseController // Filter by school year unless "all" if (!$isAllYears) { - $qbItems->where('i.school_year', $selectedYear); + $this->applyInventoryMovementPeriodFilter($qbItems, 'i', $selectedYear); } // Optional: filter by type if provided @@ -1123,7 +1152,7 @@ class InventoryController extends BaseController return view('inventory/summary', [ 'selectedYear' => $selectedYear, 'currentYear' => $this->schoolYear, - 'schoolYears' => $this->getSchoolYearsForType('book') ?: [$this->schoolYear, 'All'], + 'schoolYears' => $this->getSchoolYearsForType($selectedType) ?: [$this->schoolYear, 'All'], 'rows' => $rows, 'totals' => $totals, // optionally pass the selected type if your view supports it @@ -1202,7 +1231,7 @@ class InventoryController extends BaseController } // -------- Year dropdown options -------- - $schoolYears = $this->getSchoolYearsForType('book') ?: [$this->schoolYear, 'All']; + $schoolYears = $this->getSchoolYearsForType($selectedType) ?: [$this->schoolYear, 'All']; return view('inventory/summary', [ 'selectedYear' => $selectedYear, diff --git a/app/Controllers/View/ParentController.php b/app/Controllers/View/ParentController.php index 4f91788..c441804 100644 --- a/app/Controllers/View/ParentController.php +++ b/app/Controllers/View/ParentController.php @@ -1487,8 +1487,6 @@ $existing = $this->studentModel 'cellphone' => $phone, 'email' => $email, 'relation' => $relation, - 'semester' => $semester, - 'school_year' => $schoolYear, 'updated_at' => utc_now(), ]; @@ -1568,8 +1566,6 @@ $existing = $this->studentModel 'cellphone' => $phone, 'email' => $email, 'relation' => $relation, - 'semester' => $semester, - 'school_year' => $schoolYear, ]); } } diff --git a/app/Database/Migrations/2026-07-12-052000_GrantSubjectCurriculumPermission.php b/app/Database/Migrations/2026-07-12-052000_GrantSubjectCurriculumPermission.php new file mode 100644 index 0000000..4ee5a6b --- /dev/null +++ b/app/Database/Migrations/2026-07-12-052000_GrantSubjectCurriculumPermission.php @@ -0,0 +1,145 @@ +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(); + } +} diff --git a/app/Views/family/index.php b/app/Views/family/index.php index 04d1f65..29792bd 100644 --- a/app/Views/family/index.php +++ b/app/Views/family/index.php @@ -305,6 +305,23 @@ const normalize = s => (s||'').toString().toLowerCase(); 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) const STATIC_ITEMS = [ @@ -368,12 +385,7 @@ btn.className = 'list-group-item list-group-item-action'; btn.innerHTML = `