recreate project

This commit is contained in:
root
2026-02-10 22:11:06 -05:00
commit 663c0cdbda
10149 changed files with 1379710 additions and 0 deletions
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEventCategoryToEvents extends Migration
{
public function up()
{
$fields = $this->db->getFieldNames('events');
if (!in_array('event_category', $fields, true)) {
$this->forge->addColumn('events', [
'event_category' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
'after' => 'event_name',
'comment' => 'Admin-only category label',
],
]);
}
}
public function down()
{
$fields = $this->db->getFieldNames('events');
if (in_array('event_category', $fields, true)) {
$this->forge->dropColumn('events', 'event_category');
}
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddBatchNumberToReimbursements extends Migration
{
public function up()
{
$fields = [
'batch_number' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'default' => 0,
'null' => false,
'after' => 'reimbursement_method',
],
];
$this->forge->addColumn('reimbursements', $fields);
}
public function down()
{
$this->forge->dropColumn('reimbursements', 'batch_number');
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddYearlyBatchNumberToReimbursementBatches extends Migration
{
public function up()
{
$fields = [
'yearly_batch_number' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'default' => 0,
'null' => false,
'after' => 'title',
],
];
$this->forge->addColumn('reimbursement_batches', $fields);
$this->fillExistingBatchNumbers();
}
public function down()
{
$this->forge->dropColumn('reimbursement_batches', 'yearly_batch_number');
}
private function fillExistingBatchNumbers(): void
{
$this->db->query("SET @prev_school_year := '', @row_number := 0");
$this->db->query(
'UPDATE reimbursement_batches b
JOIN (
SELECT id,
(@row_number := IF(@prev_school_year = COALESCE(school_year, \'\'), @row_number + 1, 1)) AS seq,
@prev_school_year := COALESCE(school_year, \'\') AS school_year_marker
FROM reimbursement_batches
ORDER BY COALESCE(school_year, \'\'), COALESCE(opened_at, \'1970-01-01 00:00:00\'), id
) ranks ON b.id = ranks.id
SET b.yearly_batch_number = ranks.seq'
);
}
}
@@ -0,0 +1,119 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateInventoryTables extends Migration
{
public function up()
{
// --------- inventory_categories ----------
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'type' => ['type' => 'ENUM("classroom","book","office","kitchen")', 'null' => false, 'default' => 'office'],
'name' => ['type' => 'VARCHAR', 'constraint' => 120, 'null' => false],
'description' => ['type' => 'TEXT', 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['type', 'name']); // unique per type
$this->forge->createTable('inventory_categories', true, [
'ENGINE' => 'InnoDB',
]);
// --------- inventory_items ----------
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'type' => ['type' => 'ENUM("classroom","book","office","kitchen")', 'null' => false],
'category_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'name' => ['type' => 'VARCHAR', 'constraint' => 190, 'null' => false],
'description' => ['type' => 'TEXT', 'null' => true],
'quantity' => ['type' => 'INT', 'unsigned' => true, 'default' => 0],
'unit' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => true, 'default' => null],
// Classroom-only condition
'condition' => ['type' => 'ENUM("good","needs_repair","need_replace","cannot_find")', 'null' => true, 'default' => null],
// Book extras (author removed earlier)
'isbn' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => true],
'edition' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => true],
// Misc
'sku' => ['type' => 'VARCHAR', 'constraint' => 64, 'null' => true],
'notes' => ['type' => 'TEXT', 'null' => true],
// Academic tagging
'semester' => ['type' => 'ENUM("Spring","Fall")', 'null' => true],
'school_year' => ['type' => 'VARCHAR', 'constraint' => 16, 'null' => true], // e.g. 2025-2026
// Audit
'updated_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
// Indexes
$this->forge->addKey('id', true);
$this->forge->addKey(['type', 'name']);
$this->forge->addKey(['school_year', 'semester']);
$this->forge->addKey('category_id'); // ensure indexed for FK
$this->forge->addKey('updated_by'); // ensure indexed for FK
// Category FK (safe; category table we just created with InnoDB)
$this->forge->addForeignKey('category_id', 'inventory_categories', 'id', 'SET NULL', 'CASCADE');
// Create items table first, then add users FK conditionally
$this->forge->createTable('inventory_items', true, [
'ENGINE' => 'InnoDB',
]);
// ---- Add updated_by → users(id) FK conditionally & safely ----
$db = \Config\Database::connect();
if ($db->tableExists('users')) {
// Confirm users is InnoDB and id is an unsigned INT
$engineRow = $db->query("
SELECT ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
")->getFirstRow('array');
$colRow = $db->query("
SELECT COLUMN_TYPE, IS_NULLABLE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'id'
")->getFirstRow('array');
$isInno = isset($engineRow['ENGINE']) && strtoupper($engineRow['ENGINE']) === 'INNODB';
$isUint = isset($colRow['COLUMN_TYPE']) && stripos($colRow['COLUMN_TYPE'], 'int') !== false && stripos($colRow['COLUMN_TYPE'], 'unsigned') !== false;
if ($isInno && $isUint) {
// Add the FK now (separate ALTER avoids "incorrectly formed" during CREATE)
$db->query("
ALTER TABLE inventory_items
ADD CONSTRAINT fk_inventory_items_updated_by
FOREIGN KEY (updated_by) REFERENCES users(id)
ON DELETE SET NULL ON UPDATE CASCADE
");
}
// If not compatible, we skip the FK to avoid migration failure.
}
}
public function down()
{
// Drop children first
$db = \Config\Database::connect();
if ($db->tableExists('inventory_items')) {
// drop conditional FK if exists
try { $db->query("ALTER TABLE inventory_items DROP FOREIGN KEY fk_inventory_items_updated_by"); } catch (\Throwable $e) {}
}
$this->forge->dropTable('inventory_items', true);
$this->forge->dropTable('inventory_categories', true);
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateInventoryMovements extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type'=>'INT','unsigned'=>true,'auto_increment'=>true],
'item_id' => ['type'=>'INT','unsigned'=>true,'null'=>false],
'qty_change' => ['type'=>'INT','null'=>false], // +in / -out
'movement_type' => ['type'=>'ENUM("initial","in","out","adjust","distribution")','null'=>false,'default'=>'adjust'],
'reason' => ['type'=>'VARCHAR','constraint'=>120,'null'=>true],
'note' => ['type'=>'TEXT','null'=>true],
// tagging
'semester' => ['type'=>'ENUM("Spring","Fall")','null'=>true],
'school_year' => ['type'=>'VARCHAR','constraint'=>16,'null'=>true], // e.g. 2025-2026
// who/for whom
'performed_by' => ['type'=>'INT','unsigned'=>true,'null'=>true], // user id
'teacher_id' => ['type'=>'INT','unsigned'=>true,'null'=>true],
'student_id' => ['type'=>'INT','unsigned'=>true,'null'=>true],
'class_section_id'=> ['type'=>'INT','unsigned'=>true,'null'=>true],
'created_at' => ['type'=>'DATETIME','null'=>true],
'updated_at' => ['type'=>'DATETIME','null'=>true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('item_id');
$this->forge->addKey(['school_year','semester']);
$this->forge->addKey('movement_type');
$this->forge->addForeignKey('item_id', 'inventory_items', 'id', 'CASCADE', 'CASCADE');
// Create table as InnoDB
$this->forge->createTable('inventory_movements', true, ['ENGINE'=>'InnoDB']);
// Optional FKs to users if compatible
$db = \Config\Database::connect();
if ($db->tableExists('users')) {
// try add FK; ignore failures (engine/type mismatch)
try {
$db->query("ALTER TABLE inventory_movements
ADD CONSTRAINT fk_inv_mov_performed_by FOREIGN KEY (performed_by) REFERENCES users(id)
ON DELETE SET NULL ON UPDATE CASCADE");
} catch (\Throwable $e) {}
try {
$db->query("ALTER TABLE inventory_movements
ADD CONSTRAINT fk_inv_mov_teacher FOREIGN KEY (teacher_id) REFERENCES users(id)
ON DELETE SET NULL ON UPDATE CASCADE");
} catch (\Throwable $e) {}
}
}
public function down()
{
$this->forge->dropTable('inventory_movements', true);
}
}
@@ -0,0 +1,124 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddClassroomConditionBuckets extends Migration
{
public function up()
{
$db = \Config\Database::connect();
// --- Ensure columns exist on inventory_items ---
$cols = $db->getFieldNames('inventory_items');
// 1) Academic tagging
$addAcademic = [];
if (!in_array('school_year', $cols, true)) {
$addAcademic['school_year'] = ['type' => 'VARCHAR', 'constraint' => 16, 'null' => true];
}
if (!in_array('semester', $cols, true)) {
$addAcademic['semester'] = ['type' => 'ENUM("Spring","Fall")', 'null' => true];
}
if ($addAcademic) {
$this->forge->addColumn('inventory_items', $addAcademic);
}
// 2) Condition buckets (classroom)
$addBuckets = [];
if (!in_array('good_qty', $cols, true)) $addBuckets['good_qty'] = ['type'=>'INT','unsigned'=>true,'default'=>0,'null'=>false,'after'=>'quantity'];
if (!in_array('needs_repair_qty', $cols, true)) $addBuckets['needs_repair_qty'] = ['type'=>'INT','unsigned'=>true,'default'=>0,'null'=>false,'after'=>'good_qty'];
if (!in_array('need_replace_qty', $cols, true)) $addBuckets['need_replace_qty'] = ['type'=>'INT','unsigned'=>true,'default'=>0,'null'=>false,'after'=>'needs_repair_qty'];
if (!in_array('cannot_find_qty', $cols, true)) $addBuckets['cannot_find_qty'] = ['type'=>'INT','unsigned'=>true,'default'=>0,'null'=>false,'after'=>'need_replace_qty'];
if ($addBuckets) {
$this->forge->addColumn('inventory_items', $addBuckets);
}
// 3) Backfill school_year/semester from configuration (or fallback)
[$sem, $yr] = $this->resolveCurrentAcademic($db);
// Only fill when missing
$db->query("
UPDATE inventory_items
SET school_year = COALESCE(NULLIF(school_year, ''), ?),
semester = COALESCE(semester, ?)
WHERE school_year IS NULL OR school_year = '' OR semester IS NULL
", [$yr, $sem]);
// 4) Backfill buckets for classroom rows (first-time init)
$db->query("
UPDATE inventory_items
SET
good_qty = quantity,
needs_repair_qty = COALESCE(needs_repair_qty,0),
need_replace_qty = COALESCE(need_replace_qty,0),
cannot_find_qty = COALESCE(cannot_find_qty,0)
WHERE type='classroom'
AND (COALESCE(good_qty,0) = 0
AND COALESCE(needs_repair_qty,0) = 0
AND COALESCE(need_replace_qty,0) = 0
AND COALESCE(cannot_find_qty,0) = 0)
");
// 5) Add index on (school_year, semester) if not present
$idx = $db->query("
SELECT 1
FROM information_schema.statistics
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'inventory_items'
AND INDEX_NAME = 'idx_items_schoolyear_semester'
LIMIT 1
")->getFirstRow();
if (!$idx) {
$db->query("ALTER TABLE inventory_items ADD INDEX idx_items_schoolyear_semester (school_year, semester)");
}
}
public function down()
{
// Buckets can be dropped safely
try { $this->forge->dropColumn('inventory_items', 'good_qty'); } catch (\Throwable $e) {}
try { $this->forge->dropColumn('inventory_items', 'needs_repair_qty'); } catch (\Throwable $e) {}
try { $this->forge->dropColumn('inventory_items', 'need_replace_qty'); } catch (\Throwable $e) {}
try { $this->forge->dropColumn('inventory_items', 'cannot_find_qty'); } catch (\Throwable $e) {}
// school_year/semester may be used elsewhere; drop only if you really want to
try { $this->forge->dropColumn('inventory_items', 'school_year'); } catch (\Throwable $e) {}
try { $this->forge->dropColumn('inventory_items', 'semester'); } catch (\Throwable $e) {}
// Drop index if it exists
$db = \Config\Database::connect();
try { $db->query("ALTER TABLE inventory_items DROP INDEX idx_items_schoolyear_semester"); } catch (\Throwable $e) {}
}
/**
* Resolve current academic year/semester from configuration table,
* fallback to date-based inference if not set.
*/
private function resolveCurrentAcademic($db): array
{
$sem = null; $yr = null;
if ($db->tableExists('configuration')) {
$sem = $db->table('configuration')->select('config_value')
->where('config_key','semester')->get()->getRow('config_value');
$yr = $db->table('configuration')->select('config_value')
->where('config_key','school_year')->get()->getRow('config_value');
}
if (!$sem || !$yr) {
$now = new \DateTime('now', new \DateTimeZone('America/New_York'));
$y = (int)$now->format('Y');
$m = (int)$now->format('n');
// Adjust these rules if your academic calendar differs
$sem = $sem ?: (($m >= 2 && $m <= 5) ? 'Spring' : 'Fall');
$yr = $yr ?: (($m >= 6) ? sprintf('%d-%d', $y, $y+1) : sprintf('%d-%d', $y-1, $y));
}
return [$sem, $yr];
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddGradeRangeToBookCategories extends Migration
{
public function up()
{
$db = \Config\Database::connect();
$cols = $db->getFieldNames('inventory_categories');
$fields = [];
if (!in_array('grade_min', $cols, true)) {
$fields['grade_min'] = ['type' => 'TINYINT', 'unsigned' => true, 'null' => true, 'after' => 'description'];
}
if (!in_array('grade_max', $cols, true)) {
$fields['grade_max'] = ['type' => 'TINYINT', 'unsigned' => true, 'null' => true, 'after' => 'grade_min'];
}
if ($fields) {
$this->forge->addColumn('inventory_categories', $fields);
}
// Optional: composite index to speed up book-category lookups by range
$idx = $db->query("
SELECT 1 FROM information_schema.statistics
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME='inventory_categories'
AND INDEX_NAME='idx_cat_type_grade_range'
LIMIT 1
")->getFirstRow();
if (!$idx) {
$db->query("ALTER TABLE inventory_categories ADD INDEX idx_cat_type_grade_range (type, grade_min, grade_max)");
}
}
public function down()
{
$db = \Config\Database::connect();
try { $db->query("ALTER TABLE inventory_categories DROP INDEX idx_cat_type_grade_range"); } catch (\Throwable $e) {}
try { $this->forge->dropColumn('inventory_categories', 'grade_min'); } catch (\Throwable $e) {}
try { $this->forge->dropColumn('inventory_categories', 'grade_max'); } catch (\Throwable $e) {}
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateStaffAttendance extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type'=>'INT','unsigned'=>true,'auto_increment'=>true],
'user_id' => ['type'=>'INT','unsigned'=>true,'null'=>false],
'role_name' => ['type'=>'VARCHAR','constraint'=>64,'null'=>true],
'date' => ['type'=>'DATE','null'=>false],
'semester' => ['type'=>'VARCHAR','constraint'=>32,'null'=>false],
'school_year' => ['type'=>'VARCHAR','constraint'=>16,'null'=>false],
'status' => ['type'=>'ENUM','constraint'=>['present','absent','late'],'null'=>false,'default'=>'present'],
'reason' => ['type'=>'VARCHAR','constraint'=>255,'null'=>true],
'created_at' => ['type'=>'DATETIME','null'=>true],
'updated_at' => ['type'=>'DATETIME','null'=>true],
'created_by' => ['type'=>'INT','unsigned'=>true,'null'=>true],
'updated_by' => ['type'=>'INT','unsigned'=>true,'null'=>true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['user_id','date','semester','school_year']);
$this->forge->addForeignKey('user_id','users','id','CASCADE','CASCADE');
$this->forge->createTable('staff_attendance', true);
}
public function down()
{
$this->forge->dropTable('staff_attendance', true);
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateBadgePrintLogs extends Migration
{
public function up()
{
// Defensive: only create if not exists
if ($this->db->tableExists('badge_print_logs')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'user_id' => [
'type' => 'INT',
'constraint' => 11,
'null' => false,
],
'printed_by' => [
'type' => 'INT',
'constraint' => 11,
'null' => true,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'printed_at' => [
'type' => 'DATETIME',
'null' => false,
],
'role' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'class_section_name' => [
'type' => 'VARCHAR',
'constraint' => 191,
'null' => true,
],
'copies' => [
'type' => 'INT',
'constraint' => 11,
'null' => false,
'default' => 1,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['user_id']);
$this->forge->addKey(['school_year']);
$this->forge->createTable('badge_print_logs', true);
}
public function down()
{
if ($this->db->tableExists('badge_print_logs')) {
$this->forge->dropTable('badge_print_logs', true);
}
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateFamiliesTables extends Migration
{
public function up()
{
// families
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'family_code' => ['type' => 'VARCHAR', 'constraint' => 64, 'null' => false],
'household_name' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => true],
'address_line1' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => true],
'address_line2' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => true],
'city' => ['type' => 'VARCHAR', 'constraint' => 64, 'null' => true],
'state' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => true],
'postal_code' => ['type' => 'VARCHAR', 'constraint' => 16, 'null' => true],
'country' => ['type' => 'VARCHAR', 'constraint' => 64, 'null' => true],
'primary_phone' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => true],
'preferred_lang' => ['type' => 'VARCHAR', 'constraint' => 16, 'null' => true],
'preferred_contact_method' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => true],
'is_active' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1, 'null' => false],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('family_code');
$this->forge->createTable('families', true);
// family_students
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'family_id' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
'student_id' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
'is_primary_home' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0, 'null' => false],
'notes' => ['type' => 'TEXT', 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['family_id', 'student_id']);
$this->forge->addForeignKey('family_id', 'families', 'id', 'CASCADE', 'CASCADE');
$this->forge->addForeignKey('student_id', 'students', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('family_students', true);
// family_guardians
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'family_id' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
'user_id' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
'relation' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => false, 'default' => 'guardian'],
'is_primary' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0, 'null' => false],
'receive_emails' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1, 'null' => false],
'receive_sms' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0, 'null' => false],
'custody_notes' => ['type' => 'TEXT', 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['family_id', 'user_id']);
$this->forge->addForeignKey('family_id', 'families', 'id', 'CASCADE', 'CASCADE');
$this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('family_guardians', true);
// family_comm_prefs
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'family_id' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
'category' => ['type' => 'VARCHAR', 'constraint' => 32, 'null' => false],
'via_email' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1, 'null' => false],
'via_sms' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0, 'null' => false],
'cc_all_guardians' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1, 'null' => false],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['family_id', 'category']);
$this->forge->addForeignKey('family_id', 'families', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('family_comm_prefs', true);
}
public function down()
{
$this->forge->dropTable('family_comm_prefs', true);
$this->forge->dropTable('family_guardians', true);
$this->forge->dropTable('family_students', true);
$this->forge->dropTable('families', true);
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddNoSchoolToCalendarEvents extends Migration
{
public function up()
{
$db = \Config\Database::connect();
$cols = [];
try {
$cols = $db->getFieldNames('calendar_events');
} catch (\Throwable $e) {
$cols = [];
}
if (!in_array('no_school', $cols, true)) {
$this->forge->addColumn('calendar_events', [
'no_school' => [
'type' => 'TINYINT',
'constraint' => 1,
'null' => false,
'default' => 0,
'after' => 'notify_teacher',
],
]);
}
}
public function down()
{
try {
$this->forge->dropColumn('calendar_events', 'no_school');
} catch (\Throwable $e) {
// ignore
}
}
}
@@ -0,0 +1,83 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Forge; // optional
use CodeIgniter\Database\RawSql;
use CodeIgniter\Database\Migration;
class CreateWhatsappGroupMemberships extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'class_section_id' => [
'type' => 'INT',
'constraint' => 11,
'null' => false,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 25,
'null' => false,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => false,
],
'subject_type' => [
'type' => 'ENUM',
'constraint' => ['primary', 'second'],
'null' => false,
'default' => 'primary',
],
'subject_id' => [
'type' => 'INT',
'constraint' => 11,
'null' => false,
'comment' => 'users.id for primary; parents.id for second',
],
'is_member' => [
'type' => 'TINYINT',
'constraint' => 1,
'null' => false,
'default' => 0,
],
'verified_by' => [
'type' => 'INT',
'constraint' => 11,
'null' => true,
],
'verified_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['class_section_id', 'school_year', 'semester']);
$this->forge->addUniqueKey(['class_section_id', 'school_year', 'semester', 'subject_type', 'subject_id'], 'uniq_whatsapp_membership');
$this->forge->createTable('whatsapp_group_memberships');
}
public function down()
{
$this->forge->dropTable('whatsapp_group_memberships', true);
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateLateSlipLogs extends Migration
{
public function up()
{
if ($this->db->tableExists('late_slip_logs')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'student_name' => [
'type' => 'VARCHAR',
'constraint' => 191,
'null' => false,
],
'slip_date' => [
'type' => 'DATE',
'null' => true,
],
'time_in' => [
'type' => 'TIME',
'null' => true,
],
'grade' => [
'type' => 'VARCHAR',
'constraint' => 64,
'null' => true,
],
'reason' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'admin_name' => [
'type' => 'VARCHAR',
'constraint' => 191,
'null' => true,
],
'printed_by' => [
'type' => 'INT',
'constraint' => 11,
'null' => true,
],
'printed_at' => [
'type' => 'DATETIME',
'null' => false,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['school_year']);
$this->forge->addKey(['student_name']);
$this->forge->createTable('late_slip_logs', true);
}
public function down()
{
if ($this->db->tableExists('late_slip_logs')) {
$this->forge->dropTable('late_slip_logs', true);
}
}
}
@@ -0,0 +1,119 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePaymentNotificationLogs extends Migration
{
public function up()
{
if ($this->db->tableExists('payment_notification_logs')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'parent_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'invoice_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'period_year' => [
'type' => 'SMALLINT',
'constraint' => 4,
'null' => false,
],
'period_month' => [
'type' => 'TINYINT',
'constraint' => 2,
'null' => false,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['no_payment', 'installment'],
'default' => 'installment',
],
'to_email' => [
'type' => 'VARCHAR',
'constraint' => 191,
'null' => true,
],
'cc_email' => [
'type' => 'VARCHAR',
'constraint' => 191,
'null' => true,
],
'head_fa_notified' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'subject' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'body' => [
'type' => 'MEDIUMTEXT',
'null' => true,
],
'status' => [
'type' => 'ENUM',
'constraint' => ['sent', 'failed'],
'default' => 'sent',
],
'error_message' => [
'type' => 'TEXT',
'null' => true,
],
'balance_snapshot' => [
'type' => 'DECIMAL',
'constraint' => '10,2',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'sent_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['parent_id']);
$this->forge->addKey(['invoice_id']);
$this->forge->addKey(['school_year']);
$this->forge->addKey(['period_year', 'period_month']);
// Unique combination to prevent duplicate sends per period/type
$this->forge->addKey(['parent_id', 'period_year', 'period_month', 'type'], false, true);
$this->forge->createTable('payment_notification_logs', true);
}
public function down()
{
if ($this->db->tableExists('payment_notification_logs')) {
$this->forge->dropTable('payment_notification_logs', true);
}
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateParentAttendanceReports extends Migration
{
public function up()
{
if ($this->db->tableExists('parent_attendance_reports')) {
return; // already created
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'parent_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => false,
'comment' => 'users.id of the reporting parent (primary parent id)'
],
'student_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => false,
],
'class_section_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'report_date' => [
'type' => 'DATE',
'null' => false,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['absent', 'late', 'early_dismissal'],
'default' => 'absent',
'null' => false,
],
'arrival_time' => [
'type' => 'TIME', // for late
'null' => true,
],
'dismiss_time' => [
'type' => 'TIME', // for early dismissal
'null' => true,
],
'reason' => [
'type' => 'TEXT',
'null' => true,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 16,
'null' => true,
],
'status' => [
'type' => 'ENUM',
'constraint' => ['new','seen','processed'],
'default' => 'new',
'null' => false,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['report_date']);
$this->forge->addKey(['student_id', 'report_date']);
$this->forge->addForeignKey('parent_id', 'users', 'id', 'CASCADE', 'CASCADE');
$this->forge->addForeignKey('student_id', 'students', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('parent_attendance_reports', true);
}
public function down()
{
if ($this->db->tableExists('parent_attendance_reports')) {
$this->forge->dropTable('parent_attendance_reports', true);
}
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddSemesterToLateSlipLogs extends Migration
{
public function up()
{
// Add nullable semester column and an index with school_year if not present
if ($this->db->tableExists('late_slip_logs')) {
$fields = $this->db->getFieldNames('late_slip_logs');
if (!in_array('semester', $fields, true)) {
$this->forge->addColumn('late_slip_logs', [
'semester' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
'after' => 'school_year',
],
]);
}
// Best-effort: add composite index if missing
try {
$sql = "SHOW INDEX FROM late_slip_logs WHERE Key_name = 'idx_late_slip_year_sem'";
$exists = $this->db->query($sql)->getResultArray();
if (empty($exists)) {
$this->db->query('ALTER TABLE late_slip_logs ADD INDEX idx_late_slip_year_sem (school_year, semester)');
}
} catch (\Throwable $e) { /* ignore */ }
}
}
public function down()
{
if ($this->db->tableExists('late_slip_logs')) {
try { $this->db->query('ALTER TABLE late_slip_logs DROP INDEX idx_late_slip_year_sem'); } catch (\Throwable $e) {}
try { $this->forge->dropColumn('late_slip_logs', 'semester'); } catch (\Throwable $e) {}
}
}
}
@@ -0,0 +1,89 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePromotionQueue extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'student_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'from_class_section_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'default' => null,
],
'to_class_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'to_class_section_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'default' => null,
],
'school_year_from' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => false,
],
'school_year_to' => [
'type' => 'VARCHAR',
'constraint' => 9,
'null' => false,
],
'status' => [
'type' => 'ENUM',
'constraint' => ['queued','assigned','applied','cancelled'],
'default' => 'queued',
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
'default' => null,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
'default' => null,
],
'updated_by' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'default' => null,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['student_id', 'school_year_to'], false, true, 'uq_student_year_to');
$this->forge->addKey('to_class_id');
$this->forge->createTable('promotion_queue', true);
}
public function down()
{
$this->forge->dropTable('promotion_queue', true);
}
}
@@ -0,0 +1,82 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateEarlyDismissalSignatures extends Migration
{
public function up()
{
if ($this->db->tableExists('early_dismissal_signatures')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'report_date' => [
'type' => 'DATE',
'null' => false,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 16,
'null' => true,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'filename' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'original_name' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'mime_type' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'file_size' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'uploaded_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['report_date']);
$this->forge->addUniqueKey(['report_date', 'school_year', 'semester'], 'uniq_early_dismissal_signature');
$this->forge->createTable('early_dismissal_signatures', true);
}
public function down()
{
if ($this->db->tableExists('early_dismissal_signatures')) {
$this->forge->dropTable('early_dismissal_signatures', true);
}
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddTimezoneToPreferencesAndSettings extends Migration
{
public function up()
{
// user_preferences.timezone
if ($this->db->tableExists('user_preferences') && ! $this->db->fieldExists('timezone', 'user_preferences')) {
$this->forge->addColumn('user_preferences', [
'timezone' => [
'type' => 'VARCHAR',
'constraint' => 64,
'null' => true,
'after' => 'language',
],
]);
}
// settings.timezone (only if table exists)
if ($this->db->tableExists('settings') && ! $this->db->fieldExists('timezone', 'settings')) {
$this->forge->addColumn('settings', [
'timezone' => [
'type' => 'VARCHAR',
'constraint' => 64,
'null' => true,
'after' => 'name',
],
]);
}
}
public function down()
{
if ($this->db->tableExists('user_preferences') && $this->db->fieldExists('timezone', 'user_preferences')) {
$this->forge->dropColumn('user_preferences', 'timezone');
}
if ($this->db->tableExists('settings') && $this->db->fieldExists('timezone', 'settings')) {
$this->forge->dropColumn('settings', 'timezone');
}
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddDonationCategoryToExpenses extends Migration
{
/**
* Allow marking expenses as non-reimbursable donations.
*/
public function up()
{
$this->forge->modifyColumn('expenses', [
'category' => [
'type' => "ENUM('Expense','Purchase','Reimbursement','Donation')",
'null' => false,
],
]);
}
/**
* Revert to the original enum; donation rows are coerced back to Expense.
*/
public function down()
{
// Convert any donation rows to Expense to keep rollback safe.
$this->db->table('expenses')
->where('category', 'Donation')
->set('category', 'Expense')
->update();
$this->forge->modifyColumn('expenses', [
'category' => [
'type' => "ENUM('Expense','Purchase','Reimbursement')",
'null' => false,
],
]);
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateReimbursementBatchAdminFiles extends Migration
{
public function up()
{
$fields = [
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'batch_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'admin_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
'default' => 0,
],
'filename' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'original_filename' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'uploaded_at' => [
'type' => 'DATETIME',
'null' => false,
],
'uploaded_by' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
];
$this->forge->addField($fields);
$this->forge->addKey('id', true);
$this->forge->addKey('batch_id');
$this->forge->addKey('admin_id');
$this->forge->addUniqueKey(['batch_id', 'admin_id']);
$this->forge->createTable('reimbursement_batch_admin_files');
}
public function down()
{
$this->forge->dropTable('reimbursement_batch_admin_files', true);
}
}
@@ -0,0 +1,83 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddReviewFieldsToScoreComments extends Migration
{
public function up()
{
if (!$this->db->tableExists('score_comments')) {
return;
}
$fields = $this->db->getFieldNames('score_comments');
if (!in_array('comment_review', $fields, true)) {
$this->forge->addColumn('score_comments', [
'comment_review' => [
'type' => 'TEXT',
'null' => true,
'after' => 'comment',
],
]);
}
if (!in_array('reviewed_by', $fields, true)) {
$this->forge->addColumn('score_comments', [
'reviewed_by' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'comment_review',
],
]);
}
// Allow comments to be nullable so reviewers can attach feedback without a comment body
if (in_array('comment', $fields, true)) {
try {
$this->forge->modifyColumn('score_comments', [
'comment' => [
'type' => 'TEXT',
'null' => true,
],
]);
} catch (\Throwable $e) {
// Best effort only
}
}
}
public function down()
{
if (!$this->db->tableExists('score_comments')) {
return;
}
try {
$this->forge->modifyColumn('score_comments', [
'comment' => [
'type' => 'TEXT',
'null' => false,
],
]);
} catch (\Throwable $e) {
// Ignore rollback errors
}
try {
$this->forge->dropColumn('score_comments', 'reviewed_by');
} catch (\Throwable $e) {
// Ignore rollback errors
}
try {
$this->forge->dropColumn('score_comments', 'comment_review');
} catch (\Throwable $e) {
// Ignore rollback errors
}
}
}
@@ -0,0 +1,175 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateAttendanceCommentTemplates extends Migration
{
public function up()
{
if ($this->db->tableExists('attendance_comment_template')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'min_score' => [
'type' => 'DECIMAL',
'constraint' => '5,2',
'null' => false,
],
'max_score' => [
'type' => 'DECIMAL',
'constraint' => '5,2',
'null' => false,
],
'template_text' => [
'type' => 'TEXT',
'null' => false,
],
'is_active' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['min_score', 'max_score']);
$this->forge->addKey('is_active');
$this->forge->createTable('attendance_comment_template', true);
$now = date('Y-m-d H:i:s');
$rows = [
[
'min_score' => 100.00,
'max_score' => 100.00,
'template_text' => '{name} has maintained perfect attendance this semester, showing exceptional commitment and reliability. Outstanding consistency in attending every class session without fail. Keep it up and stay strong - bravo!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 95.00,
'max_score' => 99.99,
'template_text' => '{name} has maintained excellent attendance this semester, demonstrating strong commitment and reliability. Only rare absences, with a consistent routine of being present for class. Keep up the great consistency - well done!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 90.00,
'max_score' => 94.99,
'template_text' => '{name} has demonstrated very good attendance this semester, reflecting dependable responsibility. Missed only a few class sessions and remains consistent overall. Keep striving for even stronger attendance - great work!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 85.00,
'max_score' => 89.99,
'template_text' => '{name} has maintained good attendance this semester and displays responsible habits. A few absences were noted, yet overall consistency is solid. Aim for even fewer missed days to strengthen your routine - keep it going!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 80.00,
'max_score' => 84.99,
'template_text' => '{name}\'s attendance has been fairly consistent this semester, with some missed class sessions. Being present more regularly will support steady progress. You\'re on the right track - keep improving!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 75.00,
'max_score' => 79.99,
'template_text' => '{name}\'s attendance has been moderate this semester, with several absences impacting consistency. Prioritizing attendance will help strengthen routines and lead to success. Let\'s keep improving week by week - you\'ve got this!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 70.00,
'max_score' => 74.99,
'template_text' => '{name}\'s attendance has been somewhat inconsistent this semester, with missed sessions creating noticeable gaps. Focusing on regular attendance will help build a steady and reliable routine. Keep working on it - you can do it!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 65.00,
'max_score' => 69.99,
'template_text' => '{name}\'s attendance needs improvement this semester due to several missed classes. A renewed focus on being present regularly will help build stronger habits and lead to a successful finish. Keep going - you\'ve got this!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 60.00,
'max_score' => 64.99,
'template_text' => '{name}\'s attendance has been a concern this semester, with many missed classes. Making a consistent effort to attend regularly will help strengthen routines and keep learning on track. Stay focused - you can do this!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 55.00,
'max_score' => 59.99,
'template_text' => '{name}\'s attendance has been challenging this semester, with numerous absences recorded. A stronger commitment to being present each week will help build better habits and steady progress. Keep pushing forward!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 50.00,
'max_score' => 54.99,
'template_text' => '{name}\'s attendance has been inconsistent this semester, with many missed sessions. Prioritizing daily attendance will help rebuild consistency, strengthen learning routines, and support steady progress. You\'ve got this - stay committed!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 45.00,
'max_score' => 49.99,
'template_text' => '{name}\'s attendance has been irregular this semester, with several missed classes. Refocusing on regular attendance will help restore consistency and lay the foundation for success. Keep at it - you\'re capable!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
[
'min_score' => 0.00,
'max_score' => 44.99,
'template_text' => '{name}\'s attendance has been significantly limited this semester, with many absences. Starting fresh with consistent attendance can help you rebuild strong habits and finish the term successfully. A new start - let\'s go!',
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
],
];
$this->db->table('attendance_comment_template')->insertBatch($rows);
}
public function down()
{
if (!$this->db->tableExists('attendance_comment_template')) {
return;
}
$this->forge->dropTable('attendance_comment_template', true);
}
}
@@ -0,0 +1,74 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePrintRequests extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'teacher_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'admin_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
'class_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'file_path' => [
'type' => 'VARCHAR',
'constraint' => '255',
],
'num_copies' => [
'type' => 'INT',
'constraint' => 11,
],
'required_by' => [
'type' => 'DATETIME',
],
'pickup_method' => [
'type' => 'VARCHAR',
'constraint' => '255',
],
'status' => [
'type' => 'ENUM',
'constraint' => ['not_assigned', 'assigned', 'done', 'delivered'],
'default' => 'not_assigned',
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('teacher_id', 'users', 'id', 'CASCADE', 'CASCADE');
$this->forge->addForeignKey('admin_id', 'users', 'id', 'CASCADE', 'SET NULL');
$this->forge->addForeignKey('class_id', 'classes', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('print_requests');
}
public function down()
{
$this->forge->dropTable('print_requests');
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class FixPrintRequestsForeignKey extends Migration
{
public function up()
{
// Drop the old foreign key if it exists
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
// Add the new foreign key
$this->forge->addForeignKey('class_id', 'class_sections', 'id', 'CASCADE', 'CASCADE');
// Process the modifications
$this->forge->processIndexes('print_requests');
}
public function down()
{
// Drop the new foreign key
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
// Re-add the old foreign key
$this->forge->addForeignKey('class_id', 'classes', 'id', 'CASCADE', 'CASCADE');
// Process the modifications
$this->forge->processIndexes('print_requests');
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RevertPrintRequestsForeignKey extends Migration
{
public function up()
{
// Drop the old foreign key if it exists
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
// Add the new foreign key
$this->forge->addForeignKey('class_id', 'classes', 'id', 'CASCADE', 'CASCADE');
// Process the modifications
$this->forge->processIndexes('print_requests');
}
public function down()
{
// Drop the new foreign key
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
// Re-add the old foreign key
$this->forge->addForeignKey('class_id', 'class_sections', 'id', 'CASCADE', 'CASCADE');
// Process the modifications
$this->forge->processIndexes('print_requests');
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class DropPrintRequestsForeignKey extends Migration
{
public function up()
{
// Drop the old foreign key if it exists
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
}
public function down()
{
// Re-add the old foreign key
$this->forge->addForeignKey('class_id', 'class_sections', 'id', 'CASCADE', 'CASCADE');
// Process the modifications
$this->forge->processIndexes('print_requests');
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class FixPrintRequestsForeignKeyOnceAndForAll extends Migration
{
public function up()
{
$db = \Config\Database::connect();
$keys = $db->getForeignKeyData('print_requests');
foreach ($keys as $key) {
if ($key->constraint_name === 'print_requests_class_id_foreign') {
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
break;
}
}
$this->forge->addForeignKey('class_id', 'classSection', 'id', 'CASCADE', 'CASCADE');
$this->forge->processIndexes('print_requests');
}
public function down()
{
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class FixPrintRequestsForeignKeyAgain extends Migration
{
public function up()
{
$db = \Config\Database::connect();
$keys = $db->getForeignKeyData('print_requests');
foreach ($keys as $key) {
if ($key->constraint_name === 'print_requests_class_id_foreign') {
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
break;
}
}
$this->forge->addForeignKey('class_id', 'classSection', 'class_id', 'CASCADE', 'CASCADE');
$this->forge->processIndexes('print_requests');
}
public function down()
{
$this->forge->dropForeignKey('print_requests', 'print_requests_class_id_foreign');
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateCompetitions extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'title' => ['type' => 'VARCHAR', 'constraint' => 150],
'semester' => ['type' => 'VARCHAR', 'constraint' => 30, 'null' => true],
'school_year' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true],
'class_section_id' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'start_date' => ['type' => 'DATE', 'null' => true],
'end_date' => ['type' => 'DATE', 'null' => true],
'winners_count' => ['type' => 'INT', 'constraint' => 11, 'default' => 3],
'prize_per_point' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'default' => 1],
'is_published' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
'published_at' => ['type' => 'DATETIME', 'null' => true],
'created_by' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['class_section_id']);
$this->forge->addKey(['is_published']);
$this->forge->createTable('competitions', true);
}
public function down()
{
$this->forge->dropTable('competitions', true);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateCompetitionScores extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'competition_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'student_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'class_section_id' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'score' => ['type' => 'DECIMAL', 'constraint' => '6,2', 'default' => 0],
'notes' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['competition_id']);
$this->forge->addKey(['student_id']);
$this->forge->addKey(['class_section_id']);
$this->forge->addUniqueKey(['competition_id', 'student_id', 'class_section_id']);
$this->forge->createTable('competition_scores', true);
}
public function down()
{
$this->forge->dropTable('competition_scores', true);
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateCompetitionWinners extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'competition_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'student_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'class_section_id' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'rank' => ['type' => 'INT', 'constraint' => 11],
'score' => ['type' => 'DECIMAL', 'constraint' => '6,2', 'default' => 0],
'prize_amount' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'default' => 0],
'created_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['competition_id']);
$this->forge->addKey(['class_section_id']);
$this->forge->addUniqueKey(['competition_id', 'class_section_id', 'rank']);
$this->forge->addUniqueKey(['competition_id', 'class_section_id', 'student_id']);
$this->forge->createTable('competition_winners', true);
}
public function down()
{
$this->forge->dropTable('competition_winners', true);
}
}
@@ -0,0 +1,139 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCompetitionNavItems extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->tableExists('nav_items')) {
return;
}
$parentColumn = null;
if ($db->fieldExists('menu_parent_id', 'nav_items')) {
$parentColumn = 'menu_parent_id';
} elseif ($db->fieldExists('parent_id', 'nav_items')) {
$parentColumn = 'parent_id';
}
if ($parentColumn === null) {
return;
}
$navBuilder = $db->table('nav_items');
$now = date('Y-m-d H:i:s');
$eventRow = $navBuilder
->select('id')
->where('label', 'Event Management')
->where($parentColumn, null)
->get()
->getRowArray();
if ($eventRow) {
$eventId = (int) $eventRow['id'];
} else {
$navBuilder->insert([
$parentColumn => null,
'label' => 'Event Management',
'url' => null,
'sort_order' => 110,
'is_enabled' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
$eventId = (int) $db->insertID();
}
$competitionUrl = 'admin/competition-winners';
$existing = $navBuilder->select('id')
->where('url', $competitionUrl)
->get()
->getRowArray();
if (!$existing) {
$navBuilder->insert([
$parentColumn => $eventId ?: null,
'label' => 'Competition Winners',
'url' => $competitionUrl,
'sort_order' => 3,
'is_enabled' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
$competitionId = (int) $db->insertID();
} else {
$competitionId = (int) $existing['id'];
}
if (!$db->tableExists('role_nav_items') || !$db->tableExists('roles')) {
return;
}
if ($competitionId <= 0) {
return;
}
$roleRows = $db->table('roles')
->select('id, name')
->whereIn('name', ['administrator', 'principal', 'vice_principal'])
->get()
->getResultArray();
$roleMap = $db->table('role_nav_items');
foreach ($roleRows as $role) {
$roleId = (int) ($role['id'] ?? 0);
if ($roleId <= 0) {
continue;
}
$exists = $roleMap
->where('role_id', $roleId)
->where('nav_item_id', $competitionId)
->get()
->getRowArray();
if ($exists) {
continue;
}
$roleMap->insert([
'role_id' => $roleId,
'nav_item_id' => $competitionId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
public function down()
{
$db = \Config\Database::connect();
if (!$db->tableExists('nav_items')) {
return;
}
$competitionUrl = 'admin/competition-winners';
$row = $db->table('nav_items')
->select('id')
->where('url', $competitionUrl)
->get()
->getRowArray();
if ($row && $db->tableExists('role_nav_items')) {
$db->table('role_nav_items')
->where('nav_item_id', (int) $row['id'])
->delete();
}
$db->table('nav_items')
->where('url', $competitionUrl)
->delete();
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateCompetitionClassWinners extends Migration
{
public function up()
{
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'competition_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'class_section_id' => ['type' => 'INT', 'constraint' => 11],
'winners_override' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'question_count' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
'prize_1' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_2' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_3' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_4' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_5' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_6' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['competition_id', 'class_section_id']);
$this->forge->createTable('competition_class_winners', true);
}
public function down()
{
$this->forge->dropTable('competition_class_winners', true);
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddQuestionCountToCompetitionClassWinners extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competition_class_winners')) {
return;
}
if ($db->fieldExists('question_count', 'competition_class_winners')) {
return;
}
$this->forge->addColumn('competition_class_winners', [
'question_count' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
]);
}
public function down()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competition_class_winners')) {
return;
}
if (!$db->fieldExists('question_count', 'competition_class_winners')) {
return;
}
$this->forge->dropColumn('competition_class_winners', 'question_count');
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPrizeColumnsToCompetitionClassWinners extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competition_class_winners')) {
return;
}
$columns = [
'prize_1' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_2' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_3' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_4' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_5' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
'prize_6' => ['type' => 'DECIMAL', 'constraint' => '10,2', 'null' => true],
];
$add = [];
foreach ($columns as $name => $def) {
if (!$db->fieldExists($name, 'competition_class_winners')) {
$add[$name] = $def;
}
}
if (!empty($add)) {
$this->forge->addColumn('competition_class_winners', $add);
}
}
public function down()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competition_class_winners')) {
return;
}
$names = ['prize_1','prize_2','prize_3','prize_4','prize_5','prize_6'];
foreach ($names as $name) {
if ($db->fieldExists($name, 'competition_class_winners')) {
$this->forge->dropColumn('competition_class_winners', $name);
}
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AllowCompetitionWinnerRankTies extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competition_winners')) {
return;
}
if ($db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) {
$db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners');
}
if (!$db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) {
$db->query(
'CREATE INDEX competition_id_class_section_id_rank ON competition_winners (competition_id, class_section_id, rank)'
);
}
}
public function down()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competition_winners')) {
return;
}
if ($db->indexExists('competition_winners', 'competition_id_class_section_id_rank')) {
$db->query('DROP INDEX competition_id_class_section_id_rank ON competition_winners');
}
$db->query(
'ALTER TABLE competition_winners ADD UNIQUE KEY competition_id_class_section_id_rank (competition_id, class_section_id, rank)'
);
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateAdminNotificationSubjects extends Migration
{
public function up()
{
if ($this->db->tableExists('admin_notification_subjects')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'admin_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'subject' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => false,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('admin_id');
$this->forge->addUniqueKey(['admin_id', 'subject']);
$this->forge->createTable('admin_notification_subjects', true);
}
public function down()
{
if (!$this->db->tableExists('admin_notification_subjects')) {
return;
}
$this->forge->dropTable('admin_notification_subjects', true);
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCompetitionLockFields extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competitions')) {
return;
}
$fields = [];
if (!$db->fieldExists('is_locked', 'competitions')) {
$fields['is_locked'] = ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0];
}
if (!$db->fieldExists('locked_at', 'competitions')) {
$fields['locked_at'] = ['type' => 'DATETIME', 'null' => true];
}
if (!$db->fieldExists('locked_by', 'competitions')) {
$fields['locked_by'] = ['type' => 'INT', 'constraint' => 11, 'null' => true];
}
if (!empty($fields)) {
$this->forge->addColumn('competitions', $fields);
}
}
public function down()
{
$db = \Config\Database::connect();
if (!$db->tableExists('competitions')) {
return;
}
$columns = [];
foreach (['is_locked', 'locked_at', 'locked_by'] as $column) {
if ($db->fieldExists($column, 'competitions')) {
$columns[] = $column;
}
}
if (!empty($columns)) {
$this->forge->dropColumn('competitions', $columns);
}
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddStudentActiveFlag extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->tableExists('students')) {
return;
}
if (!$db->fieldExists('is_active', 'students')) {
$this->forge->addColumn('students', [
'is_active' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
'null' => false,
],
]);
}
}
public function down()
{
$db = \Config\Database::connect();
if (!$db->tableExists('students')) {
return;
}
if ($db->fieldExists('is_active', 'students')) {
$this->forge->dropColumn('students', 'is_active');
}
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateClassProgressReports extends Migration
{
public function up()
{
if ($this->db->tableExists('class_progress_reports')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'class_section_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'teacher_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'week_start' => ['type' => 'DATE'],
'week_end' => ['type' => 'DATE'],
'subject' => ['type' => 'VARCHAR', 'constraint' => 160, 'null' => true],
'unit_title' => ['type' => 'VARCHAR', 'constraint' => 120, 'null' => true],
'covered' => ['type' => 'TEXT'],
'homework' => ['type' => 'TEXT', 'null' => true],
'assessment' => ['type' => 'TEXT', 'null' => true],
'status' => ['type' => 'VARCHAR', 'constraint' => 20],
'status_notes' => ['type' => 'VARCHAR', 'constraint' => 200, 'null' => true],
'class_notes' => ['type' => 'TEXT', 'null' => true],
'next_week_plan' => ['type' => 'TEXT'],
'support_needed' => ['type' => 'TEXT', 'null' => true],
'flags_json' => ['type' => 'TEXT', 'null' => true],
'attachment_path' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['class_section_id', 'week_start', 'week_end']);
$this->forge->createTable('class_progress_reports');
}
public function down()
{
$this->forge->dropTable('class_progress_reports', true);
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateSubjectCurriculumItems extends Migration
{
public function up()
{
if ($this->db->tableExists('subject_curriculum_items')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'class_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'subject' => [
'type' => 'ENUM',
'constraint' => ['islamic', 'quran'],
'default' => 'islamic',
],
'unit_number' => [
'type' => 'SMALLINT',
'null' => true,
],
'unit_title' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'chapter_name' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['class_id', 'subject']);
$this->forge->createTable('subject_curriculum_items');
}
public function down()
{
$this->forge->dropTable('subject_curriculum_items', true);
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RemoveClassAssignmentSemester extends Migration
{
public function up()
{
if ($this->db->tableExists('teacher_class')) {
$this->forge->dropColumn('teacher_class', 'semester');
}
if ($this->db->tableExists('student_class')) {
$this->forge->dropColumn('student_class', 'semester');
}
}
public function down()
{
if ($this->db->tableExists('teacher_class')) {
$this->forge->addColumn('teacher_class', [
'semester' => [
'type' => 'VARCHAR',
'constraint' => 25,
'null' => false,
'default' => '',
],
]);
}
if ($this->db->tableExists('student_class')) {
$this->forge->addColumn('student_class', [
'semester' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
'default' => '',
],
]);
}
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateTeacherSubmissionNotificationHistory extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'teacher_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => false,
],
'class_section_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => false,
],
'admin_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => false,
],
'notification_category' => [
'type' => 'VARCHAR',
'constraint' => 64,
'default' => 'teacher_submissions',
],
'message' => [
'type' => 'TEXT',
'null' => true,
],
'status' => [
'type' => 'VARCHAR',
'constraint' => 32,
'default' => 'sent',
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'sent_at' => [
'type' => 'DATETIME',
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['teacher_id', 'class_section_id']);
$this->forge->createTable('teacher_submission_notification_history');
}
public function down()
{
$this->forge->dropTable('teacher_submission_notification_history');
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEventTypeToCalendarEvents extends Migration
{
public function up()
{
$db = \Config\Database::connect();
if (!$db->fieldExists('event_type', 'calendar_events')) {
$this->forge->addColumn('calendar_events', [
'event_type' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
'default' => null,
'after' => 'description',
],
]);
}
}
public function down()
{
$db = \Config\Database::connect();
if ($db->fieldExists('event_type', 'calendar_events')) {
$this->forge->dropColumn('calendar_events', 'event_type');
}
}
}
@@ -0,0 +1,109 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateExamDraftSubmissions extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'teacher_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => false,
],
'class_section_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => false,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => false,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => false,
],
'exam_type' => [
'type' => 'VARCHAR',
'constraint' => 64,
'null' => true,
],
'draft_title' => [
'type' => 'VARCHAR',
'constraint' => 191,
'null' => false,
],
'description' => [
'type' => 'TEXT',
'null' => true,
],
'teacher_file' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'teacher_filename' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'status' => [
'type' => 'VARCHAR',
'constraint' => 32,
'default' => 'pending',
],
'admin_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'admin_comments' => [
'type' => 'TEXT',
'null' => true,
],
'reviewed_at' => [
'type' => 'DATETIME',
'null' => true,
],
'final_file' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'final_filename' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => false,
'default' => 'CURRENT_TIMESTAMP',
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['teacher_id', 'class_section_id']);
$this->forge->createTable('exam_drafts');
}
public function down()
{
$this->forge->dropTable('exam_drafts');
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddVersionToExamDrafts extends Migration
{
public function up()
{
$this->forge->addColumn('exam_drafts', [
'version' => [
'type' => 'INT',
'unsigned' => true,
'default' => 1,
],
'previous_draft_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
]);
$this->forge->addKey('version');
}
public function down()
{
$this->forge->dropColumn('exam_drafts', ['version', 'previous_draft_id']);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddFinalPdfToExamDrafts extends Migration
{
public function up()
{
$this->forge->addColumn('exam_drafts', [
'final_pdf_file' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
'after' => 'final_filename',
],
]);
}
public function down()
{
$this->forge->dropColumn('exam_drafts', 'final_pdf_file');
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddIsLegacyToExamDrafts extends Migration
{
public function up()
{
$this->forge->addColumn('exam_drafts', [
'is_legacy' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'after' => 'admin_id',
],
]);
}
public function down()
{
$this->forge->dropColumn('exam_drafts', 'is_legacy');
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddClassSectionIdToScoreComments extends Migration
{
public function up()
{
if (!$this->db->tableExists('score_comments')) {
return;
}
$fields = $this->db->getFieldNames('score_comments');
if (!in_array('class_section_id', $fields, true)) {
$this->forge->addColumn('score_comments', [
'class_section_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'student_id',
],
]);
try {
$this->db->query('CREATE INDEX idx_score_comments_class_section ON score_comments(class_section_id)');
} catch (\Throwable $e) {
// Index may already exist; ignore
}
}
if (in_array('subject_id', $fields, true)) {
try {
$this->forge->dropColumn('score_comments', 'subject_id');
} catch (\Throwable $e) {
// ignore if drop fails
}
}
}
public function down()
{
if (!$this->db->tableExists('score_comments')) {
return;
}
$fields = $this->db->getFieldNames('score_comments');
if (!in_array('subject_id', $fields, true)) {
$this->forge->addColumn('score_comments', [
'subject_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'student_id',
],
]);
}
if (in_array('class_section_id', $fields, true)) {
try {
$this->forge->dropColumn('score_comments', 'class_section_id');
} catch (\Throwable $e) {
// ignore if drop fails
}
}
}
}
@@ -0,0 +1,104 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateParentMeetingSchedules extends Migration
{
public function up()
{
if ($this->db->tableExists('parent_meeting_schedules')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'student_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'parent_user_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
'parent_name' => [
'type' => 'VARCHAR',
'constraint' => 200,
'null' => true,
],
'student_name' => [
'type' => 'VARCHAR',
'constraint' => 200,
'null' => true,
],
'class_section_name' => [
'type' => 'VARCHAR',
'constraint' => 200,
'null' => true,
],
'date' => [
'type' => 'DATE',
],
'time' => [
'type' => 'TIME',
'null' => true,
],
'notes' => [
'type' => 'TEXT',
'null' => true,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => true,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => true,
],
'status' => [
'type' => 'VARCHAR',
'constraint' => 20,
'default' => 'scheduled',
],
'created_by' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('student_id');
$this->forge->addKey('parent_user_id');
$this->forge->addKey('date');
$this->forge->createTable('parent_meeting_schedules', true);
}
public function down()
{
if ($this->db->tableExists('parent_meeting_schedules')) {
$this->forge->dropTable('parent_meeting_schedules', true);
}
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateClassProgressAttachments extends Migration
{
public function up()
{
if ($this->db->tableExists('class_progress_attachments')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'report_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'file_path' => ['type' => 'VARCHAR', 'constraint' => 255],
'original_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
'mime_type' => ['type' => 'VARCHAR', 'constraint' => 120, 'null' => true],
'file_size' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('report_id');
$this->forge->createTable('class_progress_attachments');
}
public function down()
{
$this->forge->dropTable('class_progress_attachments', true);
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePlacementLevels extends Migration
{
public function up()
{
if ($this->db->tableExists('placement_levels')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'student_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'level' => ['type' => 'SMALLINT', 'constraint' => 3, 'unsigned' => true],
'school_year' => ['type' => 'VARCHAR', 'constraint' => 20],
'created_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'updated_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['student_id', 'school_year'], true);
$this->forge->addKey('school_year');
$this->forge->createTable('placement_levels');
}
public function down()
{
$this->forge->dropTable('placement_levels', true);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class UpdatePlacementLevelRange extends Migration
{
public function up()
{
if (!$this->db->tableExists('placement_levels')) {
return;
}
$fields = $this->db->getFieldNames('placement_levels');
if (!in_array('level', $fields, true)) {
return;
}
$this->forge->modifyColumn('placement_levels', [
'level' => ['type' => 'SMALLINT', 'constraint' => 3, 'unsigned' => true],
]);
}
public function down()
{
if (!$this->db->tableExists('placement_levels')) {
return;
}
$fields = $this->db->getFieldNames('placement_levels');
if (!in_array('level', $fields, true)) {
return;
}
$this->forge->modifyColumn('placement_levels', [
'level' => ['type' => 'TINYINT', 'constraint' => 1, 'unsigned' => true],
]);
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePlacementBatches extends Migration
{
public function up()
{
if ($this->db->tableExists('placement_batches')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'placement_test' => ['type' => 'VARCHAR', 'constraint' => 30],
'school_year' => ['type' => 'VARCHAR', 'constraint' => 20],
'created_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'updated_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['placement_test', 'school_year']);
$this->forge->createTable('placement_batches');
}
public function down()
{
$this->forge->dropTable('placement_batches', true);
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePlacementScores extends Migration
{
public function up()
{
if ($this->db->tableExists('placement_scores')) {
return;
}
$this->forge->addField([
'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'batch_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'student_id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true],
'score' => ['type' => 'SMALLINT', 'constraint' => 3, 'unsigned' => true],
'created_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'updated_by' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['batch_id', 'student_id'], true);
$this->forge->addKey('batch_id');
$this->forge->createTable('placement_scores');
}
public function down()
{
$this->forge->dropTable('placement_scores', true);
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEventOnlyToStudentClass extends Migration
{
public function up()
{
if (!$this->db->tableExists('student_class')) {
return;
}
$fields = $this->db->getFieldNames('student_class');
if (in_array('is_event_only', $fields, true)) {
return;
}
$this->forge->addColumn('student_class', [
'is_event_only' => ['type' => 'TINYINT', 'constraint' => 1, 'unsigned' => true, 'default' => 0],
]);
}
public function down()
{
if (!$this->db->tableExists('student_class')) {
return;
}
$fields = $this->db->getFieldNames('student_class');
if (!in_array('is_event_only', $fields, true)) {
return;
}
$this->forge->dropColumn('student_class', 'is_event_only');
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateGradingLocks extends Migration
{
public function up()
{
if ($this->db->tableExists('grading_locks')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'class_section_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 20,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 9,
],
'is_locked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'locked_by' => [
'type' => 'INT',
'constraint' => 11,
'null' => true,
],
'locked_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['class_section_id', 'semester', 'school_year'], false, false, 'idx_grading_locks_section_term');
$this->forge->addUniqueKey(['class_section_id', 'semester', 'school_year'], 'uniq_grading_locks_section_term');
$this->forge->createTable('grading_locks', true);
}
public function down()
{
$this->forge->dropTable('grading_locks', true);
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateMissingScoreOverrides extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'student_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'class_section_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
],
'semester' => [
'type' => 'VARCHAR',
'constraint' => 20,
],
'school_year' => [
'type' => 'VARCHAR',
'constraint' => 20,
],
'item_type' => [
'type' => 'VARCHAR',
'constraint' => 30,
],
'item_index' => [
'type' => 'INT',
'constraint' => 11,
'null' => true,
],
'is_allowed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'updated_by' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey(['class_section_id', 'semester', 'school_year', 'item_type'], false, 'missing_score_overrides_scope');
$this->forge->addKey(['student_id', 'class_section_id', 'semester', 'school_year', 'item_type', 'item_index'], true, 'missing_score_overrides_unique');
$this->forge->createTable('missing_score_overrides', true);
}
public function down()
{
$this->forge->dropTable('missing_score_overrides', true);
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddStyleToPreferences extends Migration
{
public function up()
{
if ($this->db->tableExists('user_preferences')) {
if (! $this->db->fieldExists('style_color', 'user_preferences')) {
$this->forge->addColumn('user_preferences', [
'style_color' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
'after' => 'timezone',
],
]);
}
if (! $this->db->fieldExists('menu_color', 'user_preferences')) {
$this->forge->addColumn('user_preferences', [
'menu_color' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
'after' => 'style_color',
],
]);
}
if (! $this->db->fieldExists('menu_custom_bg', 'user_preferences')) {
$this->forge->addColumn('user_preferences', [
'menu_custom_bg' => [
'type' => 'VARCHAR',
'constraint' => 16,
'null' => true,
'after' => 'menu_color',
],
]);
}
if (! $this->db->fieldExists('menu_custom_text', 'user_preferences')) {
$this->forge->addColumn('user_preferences', [
'menu_custom_text' => [
'type' => 'VARCHAR',
'constraint' => 16,
'null' => true,
'after' => 'menu_custom_bg',
],
]);
}
if (! $this->db->fieldExists('menu_custom_mode', 'user_preferences')) {
$this->forge->addColumn('user_preferences', [
'menu_custom_mode' => [
'type' => 'VARCHAR',
'constraint' => 8,
'null' => true,
'after' => 'menu_custom_text',
],
]);
}
}
}
public function down()
{
if ($this->db->tableExists('user_preferences')) {
if ($this->db->fieldExists('menu_custom_mode', 'user_preferences')) {
$this->forge->dropColumn('user_preferences', 'menu_custom_mode');
}
if ($this->db->fieldExists('menu_custom_text', 'user_preferences')) {
$this->forge->dropColumn('user_preferences', 'menu_custom_text');
}
if ($this->db->fieldExists('menu_custom_bg', 'user_preferences')) {
$this->forge->dropColumn('user_preferences', 'menu_custom_bg');
}
if ($this->db->fieldExists('menu_color', 'user_preferences')) {
$this->forge->dropColumn('user_preferences', 'menu_color');
}
if ($this->db->fieldExists('style_color', 'user_preferences')) {
$this->forge->dropColumn('user_preferences', 'style_color');
}
}
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class ExtendStaffAttendanceForTeachers extends Migration
{
public function up()
{
$this->forge->addColumn('staff_attendance', [
'class_section_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'user_id',
],
'position' => [
'type' => 'ENUM',
'constraint' => ['main','ta'],
'null' => true,
'after' => 'class_section_id',
],
]);
// Unique constraint: 1 record per user per date
$this->db->query("
ALTER TABLE staff_attendance
ADD UNIQUE KEY uniq_user_date (user_id, date)
");
}
public function down()
{
$this->forge->dropColumn('staff_attendance', 'class_section_id');
$this->forge->dropColumn('staff_attendance', 'position');
$this->db->query("ALTER TABLE staff_attendance DROP INDEX uniq_user_date");
}
}
View File
@@ -0,0 +1,251 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use CodeIgniter\Database\BaseConnection;
class FamiliesBootstrapSeeder extends Seeder
{
/** @var BaseConnection */
protected $db;
// Map your legacy `parents` table/columns
protected array $legacy = [
'table' => 'parents', // legacy table
'firstparent_id' => 'firstparent_id', // users.id (primary parent)
'second_user_id' => 'secondparent_id', // users.id of 2nd parent if present (may be 0)
'email' => 'secondparent_email',
'firstname' => 'secondparent_firstname',
'lastname' => 'secondparent_lastname',
];
public function run()
{
$this->db = \Config\Database::connect();
// While tuning, you can comment these two lines to avoid full rollback
$this->db->transStart();
[$familiesCreated, $studentsLinked, $primGuardiansLinked] = $this->bootstrapByPrimaryParents();
[$createdUsers, $secGuardiansLinked, $skippedLegacy, $legacyTotal] = $this->importLegacySecondParents();
$this->db->transComplete();
echo PHP_EOL;
echo "Families created: {$familiesCreated}\n";
echo "Students linked: {$studentsLinked}\n";
echo "Primary guardians: {$primGuardiansLinked}\n";
echo "Legacy rows scanned: {$legacyTotal}\n";
echo "Stub users created: {$createdUsers}\n";
echo "Secondary guardians: {$secGuardiansLinked}\n";
echo "Legacy rows skipped: {$skippedLegacy}\n";
echo PHP_EOL;
}
/** Create/ensure one family per primary parent (students.parent_id), link students, add primary guardian. */
protected function bootstrapByPrimaryParents(): array
{
$familiesCreated = 0; $studentsLinked = 0; $primGuardiansLinked = 0;
$primaryParents = $this->db->query(
"SELECT DISTINCT parent_id FROM students WHERE parent_id IS NOT NULL"
)->getResultArray();
foreach ($primaryParents as $row) {
$primaryUserId = (int) $row['parent_id'];
if ($primaryUserId <= 0) { continue; }
$familyId = $this->ensureFamilyForPrimaryParent($primaryUserId, $familiesCreated);
// link all kids of this primary parent
$kids = $this->db->query("SELECT id FROM students WHERE parent_id = ?", [$primaryUserId])->getResultArray();
foreach ($kids as $s) {
$studentsLinked += $this->attachStudentToFamily((int)$s['id'], $familyId);
}
// ensure primary guardian link
$primGuardiansLinked += $this->attachGuardianUser($familyId, $primaryUserId, 'primary', 1, 1, 0);
}
return [$familiesCreated, $studentsLinked, $primGuardiansLinked];
}
/** Import second parents from legacy `parents` table (by email or secondparent_id). */
protected function importLegacySecondParents(): array
{
$L = $this->legacy;
$createdUsers = 0; $linked = 0; $skipped = 0; $total = 0;
if (! $this->db->tableExists($L['table'])) {
echo "[LEGACY] Table '{$L['table']}' not found; skipping.\n";
return [0,0,0,0];
}
$fields = array_map('strtolower', array_column($this->db->getFieldData($L['table']), 'name'));
foreach (['firstparent_id','email','firstname','lastname','second_user_id'] as $k) {
if (! array_key_exists($k, $L)) continue;
$col = strtolower($L[$k]);
if ($col && ! in_array($col, $fields, true)) {
echo "[LEGACY] Missing column {$L[$k]} in {$L['table']}; skipping import.\n";
return [0,0,0,0];
}
}
$qb = $this->db->table($L['table']);
$qb->select("{$L['firstparent_id']} AS firstparent_id", false)
->select("{$L['second_user_id']} AS secondparent_id", false)
->select("{$L['email']} AS email", false)
->select("{$L['firstname']} AS firstname", false)
->select("{$L['lastname']} AS lastname", false)
->where("{$L['email']} IS NOT NULL", null, false)
->where("{$L['email']} !=", '');
$q = $qb->get();
if ($q === false) {
$err = $this->db->error();
echo "[LEGACY] Query failed: {$err['code']} {$err['message']}\n";
return [0,0,0,0];
}
$rows = $q->getResultArray();
$total = count($rows);
foreach ($rows as $r) {
$primaryUserId = (int)($r['firstparent_id'] ?? 0);
$email = trim((string)($r['email'] ?? ''));
$first = trim((string)($r['firstname'] ?? ''));
$last = trim((string)($r['lastname'] ?? ''));
$secondUserId = (int)($r['secondparent_id'] ?? 0);
if ($primaryUserId <= 0 || $email === '') { $skipped++; continue; }
// students for this primary parent
$students = $this->db->query("SELECT id FROM students WHERE parent_id = ?", [$primaryUserId])->getResultArray();
if (!$students) { $skipped++; continue; }
// resolve/create the second parent user
$userRow = null;
if ($secondUserId > 0) {
$userRow = $this->db->table('users')->select('id, firstname, lastname')->where('id', $secondUserId)->get()->getRowArray();
}
if (!$userRow) {
$userRow = $this->db->table('users')->select('id, firstname, lastname')->where('email', $email)->get()->getRowArray();
}
if (!$userRow) {
// STUB USER with safe defaults for your NOT NULL columns
$this->db->table('users')->insert([
'firstname' => $first ?: '(Parent)',
'lastname' => $last ?: '(Unspecified)',
'email' => $email,
'cellphone' => '',
'address_street' => '',
'apt' => null,
'city' => '',
'state' => '',
'zip' => '',
'accept_school_policy' => 0,
'is_verified' => 0,
'status' => 'Active',
'is_suspended' => 0,
'failed_attempts' => 0,
'password' => password_hash(bin2hex(random_bytes(8)), PASSWORD_BCRYPT),
'created_at' => utc_now(),
'updated_at' => utc_now(),
'token' => null,
'account_id' => null,
'user_type' => 'primary',
'semester' => '',
'school_year' => null,
'rfid_tag' => null,
'last_failed_at' => null,
]);
if ($this->db->affectedRows() === 0) {
$err = $this->db->error();
echo "[ERROR] users insert failed for {$email}: {$err['code']} {$err['message']}\n";
$skipped++; continue;
}
$secondUserId = (int)$this->db->insertID();
$createdUsers++;
$userRow = ['id' => $secondUserId, 'firstname' => $first, 'lastname' => $last];
} else {
$secondUserId = (int)$userRow['id'];
if (empty($userRow['firstname']) || empty($userRow['lastname'])) {
$this->db->table('users')->where('id', $secondUserId)->update([
'firstname' => $userRow['firstname'] ?: ($first ?: '(Parent)'),
'lastname' => $userRow['lastname'] ?: ($last ?: '(Unspecified)'),
]);
}
}
// link second parent to each students family
foreach ($students as $s) {
$studentId = (int)$s['id'];
$familyId = $this->familyIdFromStudentPrimary($studentId);
if (!$familyId) {
$familyId = $this->ensureFamilyForPrimaryParent($primaryUserId, $dummy = 0);
$this->attachStudentToFamily($studentId, $familyId);
}
$linked += $this->attachGuardianUser($familyId, $secondUserId, 'secondary', 0, 1, 0);
}
}
return [$createdUsers, $linked, $skipped, $total];
}
// ---------- Helpers ----------
/** Ensure there is exactly one family per primary parent user. */
protected function ensureFamilyForPrimaryParent(int $primaryUserId, int &$createdCounter): int
{
$code = 'FAM-' . $primaryUserId;
$row = $this->db->query("SELECT id FROM families WHERE family_code = ?", [$code])->getRowArray();
if ($row) return (int)$row['id'];
$this->db->table('families')->insert([
'family_code' => $code,
'household_name' => 'Family of User ' . $primaryUserId,
'is_active' => 1,
]);
$createdCounter++;
return (int)$this->db->insertID();
}
/** Idempotent link student → family. */
protected function attachStudentToFamily(int $studentId, int $familyId): int
{
$this->db->query(
"INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home) VALUES (?, ?, 1)",
[$familyId, $studentId]
);
return $this->db->affectedRows();
}
/** Idempotent link guardian user → family. */
protected function attachGuardianUser(int $familyId, int $userId, string $relation, int $isPrimary, int $receiveEmails, int $receiveSms): int
{
$this->db->query(
"INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms) VALUES (?, ?, ?, ?, ?, ?)",
[$familyId, $userId, $relation, $isPrimary, $receiveEmails, $receiveSms]
);
return $this->db->affectedRows();
}
/** Canonical family for a student based on parent_id → FAM-{parent_id}, fallback to any linked family. */
protected function familyIdFromStudentPrimary(int $studentId): ?int
{
$row = $this->db->query("SELECT parent_id FROM students WHERE id = ?", [$studentId])->getRowArray();
if (!$row || empty($row['parent_id'])) return null;
$code = 'FAM-' . (int)$row['parent_id'];
$fam = $this->db->query("SELECT id FROM families WHERE family_code = ?", [$code])->getRowArray();
if ($fam) return (int)$fam['id'];
$fam2 = $this->db->query(
"SELECT family_id FROM family_students WHERE student_id = ? ORDER BY is_primary_home DESC LIMIT 1",
[$studentId]
)->getRowArray();
return $fam2 ? (int)$fam2['family_id'] : null;
}
}
@@ -0,0 +1,306 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use CodeIgniter\Database\BaseConnection;
class FamiliesRebuildSeeder extends Seeder
{
/** @var BaseConnection */
protected $db;
/** Legacy parents table mapping (fits your schema exactly) */
protected array $legacy = [
'table' => 'parents',
'firstparent_id' => 'firstparent_id', // users.id of primary parent
'second_user_id' => 'secondparent_id', // users.id of 2nd parent (may be 0)
'second_email' => 'secondparent_email',
'second_firstname' => 'secondparent_firstname',
'second_lastname' => 'secondparent_lastname',
];
/** Defaults for prefs rows per category (your schema) */
protected array $prefsDefaults = [
// category => [via_email, via_sms, cc_all_guardians]
'attendance' => [1, 0, 1],
'grade' => [1, 0, 1],
'behavior' => [1, 0, 1],
'general' => [1, 0, 1],
];
public function run()
{
$this->db = \Config\Database::connect();
$this->db->transStrict(false); // don't auto-rollback the entire txn on one failure
$this->db->transException(false); // don't throw (so we can print errors ourselves)
// Start a transaction so the DB stays consistent; we'll print status after.
$this->db->transStart();
[$familiesCreated, $studentLinks] = $this->ensureFamiliesAndLinks();
$primaryLinked = $this->linkPrimaryGuardians();
[$stubUsers, $secondaryLinked] = $this->importSecondaryGuardiansFromParents();
$prefsUpserts = $this->seedFamilyCommPrefsPerCategory();
$this->db->transComplete();
echo PHP_EOL;
$status = $this->db->transStatus();
echo $status ? "[TXN] Transaction COMMITTED.\n" : "[TXN] Transaction FAILED.\n";
if (! $status) {
$err = $this->db->error();
echo " DB error: " . ($err['message'] ?? 'unknown') . " (code " . ($err['code'] ?? 'n/a') . ")\n";
}
echo "Families created: {$familiesCreated}\n";
echo "family_students links inserted: {$studentLinks}\n";
echo "Primary guardians linked: {$primaryLinked}\n";
echo "Stub users created (secondary): {$stubUsers}\n";
echo "Secondary guardians linked: {$secondaryLinked}\n";
echo "family_comm_prefs upserts: {$prefsUpserts}\n";
echo PHP_EOL;
}
protected function exec(string $label, string $sql): int
{
$ok = $this->db->query($sql);
if (! $ok) {
$err = $this->db->error();
echo "[ERROR] {$label}: " . ($err['message'] ?? 'unknown') . " (code " . ($err['code'] ?? 'n/a') . ")\n";
return 0;
}
$n = (int) $this->db->affectedRows();
echo "[OK] {$label}: affectedRows={$n}\n";
return $n;
}
/**
* Create families from students where parent_id maps to a real users.id,
* then link all students to their family's id.
*/
protected function ensureFamiliesAndLinks(): array
{
$familiesCreated = $this->exec(
'families insert',
"
INSERT IGNORE INTO families (family_code, household_name, is_active, created_at, updated_at)
SELECT DISTINCT
CONCAT('FAM-', s.parent_id),
CONCAT('Family of User ', s.parent_id),
1, NOW(), NOW()
FROM students s
JOIN users u ON u.id = s.parent_id
WHERE s.parent_id IS NOT NULL
"
);
$studentLinks = $this->exec(
'family_students insert',
"
INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home)
SELECT f.id, s.id, 1
FROM students s
JOIN users u ON u.id = s.parent_id
JOIN families f ON f.family_code = CONCAT('FAM-', s.parent_id)
"
);
// Backfill addresses/phone/household_name from users if empty
$this->exec(
'families backfill address/phone/name',
"
UPDATE families f
JOIN users u ON u.id = CAST(SUBSTRING(f.family_code, 5) AS UNSIGNED)
SET
f.address_line1 = COALESCE(NULLIF(f.address_line1, ''), NULLIF(u.address_street, '')),
f.city = COALESCE(NULLIF(f.city, ''), NULLIF(u.city, '')),
f.state = COALESCE(NULLIF(f.state, ''), NULLIF(u.state, '')),
f.postal_code = COALESCE(NULLIF(f.postal_code, ''), NULLIF(u.zip, '')),
f.primary_phone = COALESCE(NULLIF(f.primary_phone, ''), NULLIF(u.cellphone, '')),
f.household_name= CASE
WHEN f.household_name IS NULL OR f.household_name = ''
THEN TRIM(CONCAT('Family of ', NULLIF(u.firstname, ''), ' ', NULLIF(u.lastname, '')))
ELSE f.household_name
END
"
);
return [$familiesCreated, $studentLinks];
}
/**
* Link primary guardians using student-anchored joins (no family_code parsing assumptions).
* relation uses a safe generic 'guardian' (fits varchar(32) and common enums).
*/
protected function linkPrimaryGuardians(): int
{
// Log candidate count (already printed earlier for you)
$row = $this->db->query("
SELECT COUNT(*) AS c FROM (
SELECT DISTINCT fs.family_id, u.id AS user_id
FROM family_students fs
JOIN students s ON s.id = fs.student_id
JOIN users u ON u.id = s.parent_id
WHERE s.parent_id IS NOT NULL
) X
")->getRowArray();
echo "[DEBUG] Primary guardian candidates (via students): " . (int)($row['c'] ?? 0) . "\n";
// Path A: student-anchored (ignore duplicates/violations)
$linkedA = $this->exec(
'primary guardians insert A (student-anchored, IGNORE)',
"
INSERT IGNORE INTO family_guardians
(family_id, user_id, relation, is_primary, receive_emails, receive_sms)
SELECT DISTINCT
fs.family_id,
u.id,
'guardian', 1, 1, 0
FROM family_students fs
JOIN students s ON s.id = fs.student_id
JOIN users u ON u.id = s.parent_id
WHERE s.parent_id IS NOT NULL
"
);
// Path B: via family_code (belt & suspenders)
$linkedB = $this->exec(
'primary guardians insert B (family_code, IGNORE)',
"
INSERT IGNORE INTO family_guardians
(family_id, user_id, relation, is_primary, receive_emails, receive_sms)
SELECT f.id, u.id, 'guardian', 1, 1, 0
FROM families f
JOIN users u ON u.id = CAST(SUBSTRING(f.family_code, 5) AS UNSIGNED)
"
);
return $linkedA + $linkedB;
}
/**
* Create/link secondary guardians from legacy parents:
* - Create stub users for secondparent_email not present in users.
* - Link those (or explicit secondparent_id) to all families of the first parents students.
*/
protected function importSecondaryGuardiansFromParents(): array
{
$L = $this->legacy;
if (! $this->db->tableExists($L['table'])) {
echo "[LEGACY] Table '{$L['table']}' not found; skipping secondary import.\n";
return [0, 0];
}
$fields = array_map('strtolower', array_column($this->db->getFieldData($L['table']), 'name'));
foreach (['firstparent_id','second_user_id','second_email','second_firstname','second_lastname'] as $k) {
$col = strtolower($L[$k]);
if (! in_array($col, $fields, true)) {
echo "[LEGACY] Missing column {$L[$k]} in {$L['table']}; skipping secondary import.\n";
return [0, 0];
}
}
$stubUsers = $this->exec(
'secondary guardian stubs (users)',
"
INSERT INTO users
(firstname, lastname, email, cellphone, address_street, apt, city, state, zip,
accept_school_policy, is_verified, status, is_suspended, failed_attempts,
password, created_at, updated_at, token, account_id, user_type, semester,
school_year, rfid_tag, last_failed_at)
SELECT
NULLIF(p.{$L['second_firstname']},''),
NULLIF(p.{$L['second_lastname']},''),
p.{$L['second_email']},
'', '', NULL, '', '', '',
0, 0, 'Active', 0, 0,
'$2y$10abcdefghijklmnopqrstuv',
NOW(), NOW(), NULL, NULL, 'secondary',
'', NULL, NULL, NULL
FROM {$L['table']} p
LEFT JOIN users u ON u.email = p.{$L['second_email']}
WHERE p.{$L['second_email']} IS NOT NULL
AND p.{$L['second_email']} <> ''
AND u.id IS NULL
"
);
$secondaryLinked = $this->exec(
'secondary guardians insert (IGNORE)',
"
INSERT IGNORE INTO family_guardians
(family_id, user_id, relation, is_primary, receive_emails, receive_sms)
SELECT DISTINCT
fs.family_id,
COALESCE(u2.id, u_email.id) AS user_id,
'guardian', 0, 1, 0
FROM {$L['table']} p
JOIN students s
ON s.parent_id = p.{$L['firstparent_id']}
OR s.parent_id = p.id
JOIN family_students fs ON fs.student_id = s.id
LEFT JOIN users u2 ON u2.id = NULLIF(p.{$L['second_user_id']}, 0)
LEFT JOIN users u_email ON u_email.email = p.{$L['second_email']}
WHERE p.{$L['second_email']} IS NOT NULL
AND p.{$L['second_email']} <> ''
AND COALESCE(u2.id, u_email.id) IS NOT NULL
"
);
return [$stubUsers, $secondaryLinked];
}
/**
* Seed one prefs row per (family_id, category) with your defaults.
* Counts how many rows were *actually inserted* across all categories.
*/
protected function seedFamilyCommPrefsPerCategory(): int
{
if (! $this->db->tableExists('family_comm_prefs')) {
echo "[INFO] family_comm_prefs not found; skipping prefs seed.\n";
return 0;
}
$insertedTotal = 0;
foreach ($this->prefsDefaults as $category => [$viaEmail, $viaSms, $ccAll]) {
// Count how many families are missing this category BEFORE
$row = $this->db->query("
SELECT COUNT(*) AS c
FROM families f
LEFT JOIN family_comm_prefs p
ON p.family_id = f.id AND p.category = ?
WHERE p.family_id IS NULL
", [$category])->getRowArray();
$missing = (int)($row['c'] ?? 0);
if ($missing > 0) {
$this->db->query("
INSERT INTO family_comm_prefs (family_id, category, via_email, via_sms, cc_all_guardians)
SELECT f.id, ?, ?, ?, ?
FROM families f
LEFT JOIN family_comm_prefs p
ON p.family_id = f.id AND p.category = ?
WHERE p.family_id IS NULL
", [$category, (int)$viaEmail, (int)$viaSms, (int)$ccAll, $category]);
$insertedTotal += $missing;
}
}
return $insertedTotal;
}
/** Utility: check if a column exists (case-insensitive) */
protected function columnExists(string $table, string $column): bool
{
if (! $this->db->tableExists($table)) return false;
$cols = array_map('strtolower', array_column($this->db->getFieldData($table), 'name'));
return in_array(strtolower($column), $cols, true);
}
}
+179
View File
@@ -0,0 +1,179 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class NavSeeder extends Seeder
{
public function run()
{
$db = \Config\Database::connect();
// Top-level groups (dropdown headers are just parents with no URL)
$groups = [
['label'=>'Users', 'url'=>null, 'sort_order'=>10],
['label'=>'Roles', 'url'=>null, 'sort_order'=>20],
['label'=>'Configuration', 'url'=>null, 'sort_order'=>30],
['label'=>'Staffing', 'url'=>null, 'sort_order'=>40],
['label'=>'Parents', 'url'=>null, 'sort_order'=>50],
['label'=>'Student-Affairs', 'url'=>null, 'sort_order'=>60],
['label'=>'Classes', 'url'=>null, 'sort_order'=>70],
['label'=>'Communication', 'url'=>null, 'sort_order'=>80],
['label'=>'Financial', 'url'=>null, 'sort_order'=>90],
['label'=>'Printables', 'url'=>null, 'sort_order'=>100],
['label'=>'Event Management', 'url'=>null, 'sort_order'=>110],
];
$builder = $db->table('nav_items');
$ids = [];
foreach ($groups as $g) {
$builder->insert([
'parent_id' => null,
'label' => $g['label'],
'url' => null,
'sort_order' => $g['sort_order'],
'is_enabled' => 1,
'created_at' => utc_now()
]);
$ids[$g['label']] = $db->insertID();
}
// Children for Users
$items = [
['parent'=>'Users', 'label'=>'Active Notifications', 'url'=>'notifications/active', 'sort_order'=>1],
['parent'=>'Users', 'label'=>'Deleted Notifications','url'=>'notifications/deleted','sort_order'=>2],
['parent'=>'Users', 'label'=>'Login Activity', 'url'=>'user/login_activity', 'sort_order'=>3],
['parent'=>'Users', 'label'=>'User List', 'url'=>'user/user_list', 'sort_order'=>4],
// Roles
['parent'=>'Roles', 'label'=>'Assign User Role', 'url'=>'rolepermission/assign_role', 'sort_order'=>1],
['parent'=>'Roles', 'label'=>'Role Management', 'url'=>'rolepermission/roles', 'sort_order'=>2],
// Configuration
['parent'=>'Configuration','label'=>'Add/Edit Configuration','url'=>'configuration/configuration_view','sort_order'=>1],
// Staffing
['parent'=>'Staffing','label'=>'Staff Profile','url'=>'staff/index','sort_order'=>1],
['parent'=>'Staffing','label'=>'Teacher Class Assignment','url'=>'administrator/teacher_class_assignment','sort_order'=>2],
// Parents
['parent'=>'Parents','label'=>'Parent Profile','url'=>'administrator/parent_profiles','sort_order'=>1],
// Student-Affairs
['parent'=>'Student-Affairs','label'=>'Attendance Management','url'=>'administrator/daily_attendance','sort_order'=>1],
['parent'=>'Student-Affairs','label'=>'Attendance Tracking System','url'=>'attendance/violations','sort_order'=>2],
['parent'=>'Student-Affairs','label'=>'Attendance Scans','url'=>'rfid_coming_soon','sort_order'=>3],
['parent'=>'Student-Affairs','label'=>'Classes List','url'=>'administrator/class_assignment','sort_order'=>4],
['parent'=>'Student-Affairs','label'=>'Emergency Contact','url'=>'administrator/emergency_contact','sort_order'=>5],
['parent'=>'Student-Affairs','label'=>'Enrollment-Withdrawal','url'=>'enroll_withdraw/enrollment_withdrawal','sort_order'=>6],
['parent'=>'Student-Affairs','label'=>'Flags Management','url'=>'flags/flags_management','sort_order'=>7],
['parent'=>'Student-Affairs','label'=>'School Calendar','url'=>'administrator/calendar_view','sort_order'=>8],
['parent'=>'Student-Affairs','label'=>'Score Analysis','url'=>'report/combined','sort_order'=>9],
['parent'=>'Student-Affairs','label'=>'Score Management','url'=>'grading','sort_order'=>10],
['parent'=>'Student-Affairs','label'=>'Student Class Assignment','url'=>'administrator/student_class_assignment','sort_order'=>11],
['parent'=>'Student-Affairs','label'=>'Student Profile','url'=>'administrator/student_profiles','sort_order'=>12],
// Classes
['parent'=>'Classes','label'=>'Classes List','url'=>'administrator/class_assignment','sort_order'=>1],
// Communication
['parent'=>'Communication','label'=>'Parent Email Extractor','url'=>'emails/parent_email_extractor','sort_order'=>1],
['parent'=>'Communication','label'=>'Parent Profile','url'=>'administrator/parent_profiles','sort_order'=>2],
['parent'=>'Communication','label'=>'Student Profile','url'=>'administrator/student_profiles','sort_order'=>3],
// Financial
['parent'=>'Financial','label'=>'Discount Management','url'=>'discounts/list','sort_order'=>1],
['parent'=>'Financial','label'=>'Expenses Management','url'=>'expenses/index','sort_order'=>2],
['parent'=>'Financial','label'=>'Financial Report','url'=>'payment/financial_report','sort_order'=>3],
['parent'=>'Financial','label'=>'Invoices Management','url'=>'invoice_payment/invoice_management','sort_order'=>4],
['parent'=>'Financial','label'=>'Manual Payment','url'=>'payment/manual_pay','sort_order'=>5],
['parent'=>'Financial','label'=>'PaypalTransactions','url'=>'administrator/paypal_transactions','sort_order'=>6],
['parent'=>'Financial','label'=>'Refund Management','url'=>'refunds/list','sort_order'=>7],
['parent'=>'Financial','label'=>'Reimbursement Management','url'=>'reimbursements/index','sort_order'=>8],
// Printables
['parent'=>'Printables','label'=>'Badges','url'=>'printables_reports/badge_form','sort_order'=>1],
['parent'=>'Printables','label'=>'Class Prep','url'=>'class-prep','sort_order'=>2],
['parent'=>'Printables','label'=>'Report Cards','url'=>'printables_reports/report_card','sort_order'=>3],
['parent'=>'Printables','label'=>'Stickers','url'=>'printables_reports/stickers','sort_order'=>4],
// Event Management
['parent'=>'Event Management','label'=>'Calendar','url'=>'administrator/calendar','sort_order'=>1],
['parent'=>'Event Management','label'=>'Events','url'=>'administrator/events','sort_order'=>2],
];
foreach ($items as $it) {
$builder->insert([
'parent_id' => $ids[$it['parent']] ?? null,
'label' => $it['label'],
'url' => $it['url'],
'sort_order' => $it['sort_order'],
'is_enabled' => 1,
'created_at' => utc_now(),
]);
}
// Map roles quickly: give full menu to administrator & principal; a subset to others
$roleMap = $db->table('role_nav_items');
$navRows = $db->table('nav_items')->orderBy('id')->get()->getResultArray();
$grantAllTo = ['administrator','principal','vice_principal'];
foreach ($navRows as $row) {
foreach ($grantAllTo as $role) {
$roleMap->insert([
'role' => strtolower($role),
'nav_item_id' => $row['id'],
'created_at' => utc_now()
]);
}
}
// Example: "admin" (limited)
$limitForAdmin = ['Enrollment-Withdrawal','Invoices Management'];
foreach ($navRows as $row) {
if (in_array($row['label'], $limitForAdmin, true)) {
$roleMap->insert([
'role' => 'admin',
'nav_item_id' => $row['id'],
'created_at' => utc_now()
]);
}
}
// Example: HOD Communication
$hodComm = ['Communication','Parent Profile','Student Profile','Parent Email Extractor'];
foreach ($navRows as $row) {
if (in_array($row['label'], $hodComm, true)) {
$roleMap->insert([
'role' => 'head of department (communication)',
'nav_item_id' => $row['id'],
'created_at' => utc_now()
]);
}
}
// Example: HOD Education (Student-Affairs group)
$hodEduLabels = ['Student-Affairs','Attendance Management','Classes List','Emergency Contact','Enrollment-Withdrawal','Flags Management','School Calendar','Score Analysis','Score Management','Student Class Assignment','Student Profile'];
foreach ($navRows as $row) {
if (in_array($row['label'], $hodEduLabels, true)) {
$roleMap->insert([
'role' => 'head of department (education)',
'nav_item_id' => $row['id'],
'created_at' => utc_now()
]);
}
}
// Example: HOD IT (Grades)
foreach ($navRows as $row) {
if (in_array($row['label'], ['Score Management'], true)) {
$roleMap->insert([
'role' => 'head of department (information technology)',
'nav_item_id' => $row['id'],
'created_at' => utc_now()
]);
}
}
}
}
@@ -0,0 +1,159 @@
<?php
namespace App\Database\Seeds;
use App\Models\SubjectCurriculumModel;
use CodeIgniter\Database\Seeder;
class SubjectCurriculumSeeder extends Seeder
{
public function run()
{
$model = new SubjectCurriculumModel();
$db = \Config\Database::connect();
$classMap = $this->buildClassMap($db);
if (empty($classMap)) {
log_message('warning', 'SubjectCurriculumSeeder could not map any classes. Skipping import.');
return;
}
$db->table($model->table)->truncate();
$now = date('Y-m-d H:i:s');
$this->importIslamic($model, $classMap, $now);
$this->importQuran($model, $classMap, $now);
}
private function buildClassMap($db): array
{
$rows = $db->table('classes')
->select('id, class_name')
->get()
->getResultArray();
$map = [];
foreach ($rows as $row) {
$key = $this->normalizeGradeKey($row['class_name'] ?? '');
if ($key === '') {
continue;
}
$map[$key] = (int) $row['id'];
}
return $map;
}
private function importIslamic(SubjectCurriculumModel $model, array $classMap, string $now): void
{
$entries = $this->readCsv(ROOTPATH . 'levels_1_to_9_table.csv');
if (empty($entries)) {
log_message('warning', 'SubjectCurriculumSeeder could not read levels_1_to_9_table.csv.');
return;
}
$batch = [];
foreach ($entries as $row) {
$grade = $this->normalizeGradeKey($row['Grade'] ?? '');
$classId = $classMap[$grade] ?? null;
if (! $classId) {
continue;
}
$unitNumber = is_numeric($row['Unit'] ?? null) ? (int) $row['Unit'] : null;
$unitTitle = trim($row['Unit Title'] ?? '');
$chapterName = trim($row['chapter'] ?? '');
if ($chapterName === '') {
continue;
}
$batch[] = [
'class_id' => $classId,
'subject' => 'islamic',
'unit_number' => $unitNumber,
'unit_title' => $unitTitle ?: null,
'chapter_name' => $chapterName,
'created_at' => $now,
'updated_at' => $now,
];
}
if (! empty($batch)) {
$model->insertBatch($batch);
}
}
private function importQuran(SubjectCurriculumModel $model, array $classMap, string $now): void
{
$entries = $this->readCsv(ROOTPATH . 'quran_surahs_by_grade.csv');
if (empty($entries)) {
log_message('warning', 'SubjectCurriculumSeeder could not read quran_surahs_by_grade.csv.');
return;
}
$batch = [];
foreach ($entries as $row) {
$grade = $this->normalizeGradeKey($row['Grade'] ?? '');
$classId = $classMap[$grade] ?? null;
if (! $classId) {
continue;
}
$surah = trim($row['Surah'] ?? '');
if ($surah === '') {
continue;
}
$batch[] = [
'class_id' => $classId,
'subject' => 'quran',
'unit_number' => null,
'unit_title' => null,
'chapter_name' => $surah,
'created_at' => $now,
'updated_at' => $now,
];
}
if (! empty($batch)) {
$model->insertBatch($batch);
}
}
private function readCsv(string $path): array
{
if (! is_file($path)) {
return [];
}
$handle = fopen($path, 'r');
if (! $handle) {
return [];
}
$rows = [];
$headers = fgetcsv($handle);
if (! $headers) {
fclose($handle);
return [];
}
$headers = array_map('trim', $headers);
while (($data = fgetcsv($handle)) !== false) {
$row = [];
foreach ($headers as $index => $header) {
$row[$header] = $data[$index] ?? '';
}
$rows[] = $row;
}
fclose($handle);
return $rows;
}
private function normalizeGradeKey(string $value): string
{
$value = strtolower(trim($value));
$value = preg_replace('/^grade\s*/i', '', $value);
return trim($value, " \t\n\r\0\x0B");
}
}