reconstruction of the project
This commit is contained in:
+107
-78
@@ -2,15 +2,17 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AdditionalCharge extends Model
|
||||
class AdditionalCharge extends BaseModel
|
||||
{
|
||||
protected $table = 'additional_charges';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// CI useTimestamps = false
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'invoice_id',
|
||||
@@ -28,20 +30,18 @@ class AdditionalCharge extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'amount' => 'decimal:2',
|
||||
'created_by' => 'integer',
|
||||
'due_date' => 'date',
|
||||
'parent_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'amount' => 'decimal:2', // adjust if you store more precision
|
||||
'due_date' => 'date', // if due_date is DATE/DATETIME
|
||||
'created_by' => 'integer',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/* =========================
|
||||
* Relationships (optional)
|
||||
* ========================= */
|
||||
|
||||
public function parent()
|
||||
public function parentUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
@@ -51,82 +51,112 @@ class AdditionalCharge extends Model
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
/* =========================
|
||||
* Query scopes (optional)
|
||||
* ========================= */
|
||||
|
||||
public function scopeForTerm(Builder $q, string $schoolYear, ?string $semester = null): Builder
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
$q->where('school_year', $schoolYear);
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Query Scopes (CI equivalent of byParentTerm)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function scopeByParentTerm(Builder $query, int $parentId, string $year, string $semester, ?string $status = null)
|
||||
{
|
||||
$query->where('parent_id', $parentId)
|
||||
->where('school_year', $year)
|
||||
->where('semester', $semester);
|
||||
|
||||
if ($status !== null) {
|
||||
$query->where('status', $status);
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
|
||||
return $query->orderBy('id', 'DESC');
|
||||
return $q;
|
||||
}
|
||||
|
||||
public function scopeStatus(Builder $q, ?string $status): Builder
|
||||
{
|
||||
return ($status !== null && $status !== '')
|
||||
? $q->where('status', $status)
|
||||
: $q;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Methods
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
public function scopeByParentTerm(
|
||||
Builder $q,
|
||||
int $parentId,
|
||||
string $year,
|
||||
string $sem,
|
||||
?string $status = null
|
||||
): Builder {
|
||||
$q->where('parent_id', $parentId)
|
||||
->where('school_year', $year)
|
||||
->where('semester', $sem);
|
||||
|
||||
public function markApplied($ids, int $invoiceId): bool
|
||||
if ($status !== null && $status !== '') {
|
||||
$q->where('status', $status);
|
||||
}
|
||||
|
||||
return $q->orderByDesc('id');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI methods equivalents
|
||||
* ========================= */
|
||||
|
||||
public static function byParentTerm(int $parentId, string $year, string $sem, ?string $status = null)
|
||||
{
|
||||
return static::query()
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $year)
|
||||
->where('semester', $sem)
|
||||
->when($status !== null, fn ($q) => $q->where('status', $status))
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set status=applied and invoice_id for given IDs.
|
||||
* Returns true if nothing to update (empty ids) or update succeeded.
|
||||
*/
|
||||
public static function markApplied($ids, int $invoiceId): bool
|
||||
{
|
||||
$ids = is_array($ids) ? $ids : [$ids];
|
||||
$ids = array_values(array_filter(array_map('intval', $ids)));
|
||||
|
||||
if (empty($ids)) {
|
||||
if (count($ids) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ids = array_map('intval', $ids);
|
||||
|
||||
return $this->whereIn('id', $ids)
|
||||
$affected = static::query()
|
||||
->whereIn('id', $ids)
|
||||
->update([
|
||||
'status' => 'applied',
|
||||
'invoice_id' => $invoiceId
|
||||
]) >= 0;
|
||||
'status' => 'applied',
|
||||
'invoice_id' => $invoiceId,
|
||||
]);
|
||||
|
||||
return $affected >= 0; // update returns affected rows; 0 is still "ok"
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Search (CI paginate alternative)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function listAllForTerm(
|
||||
/**
|
||||
* listAllForTerm (Laravel version)
|
||||
* - joins users + invoices
|
||||
* - supports status + q search
|
||||
* - paginates
|
||||
* - fallback: if no rows for semester and allowed, retry year-only
|
||||
*/
|
||||
public static function listAllForTerm(
|
||||
string $schoolYear,
|
||||
string $semester,
|
||||
?string $status = null,
|
||||
?string $q = null,
|
||||
int $perPage = 50,
|
||||
bool $fallbackToYear = true
|
||||
)
|
||||
{
|
||||
$build = function (?string $semesterFilter) use ($schoolYear, $status, $q) {
|
||||
$query = self::query()
|
||||
->select('additional_charges.*')
|
||||
->selectRaw("CONCAT(COALESCE(users.lastname,''), ', ', COALESCE(users.firstname,'')) AS parent_name")
|
||||
->selectRaw('invoices.invoice_number')
|
||||
) {
|
||||
$build = function (?string $semFilter) use ($schoolYear, $status, $q) {
|
||||
$query = static::query()
|
||||
->select([
|
||||
'additional_charges.*',
|
||||
DB::raw("CONCAT(COALESCE(users.lastname, ''), ', ', COALESCE(users.firstname, '')) AS parent_name"),
|
||||
'invoices.invoice_number',
|
||||
])
|
||||
->leftJoin('users', 'users.id', '=', 'additional_charges.parent_id')
|
||||
->leftJoin('invoices', 'invoices.id', '=', 'additional_charges.invoice_id')
|
||||
->where('additional_charges.school_year', $schoolYear);
|
||||
|
||||
if ($semesterFilter !== null && $semesterFilter !== '') {
|
||||
$query->where('additional_charges.semester', $semesterFilter);
|
||||
if ($semFilter !== null && $semFilter !== '') {
|
||||
$query->where('additional_charges.semester', $semFilter);
|
||||
}
|
||||
|
||||
if (!empty($status)) {
|
||||
@@ -134,26 +164,25 @@ class AdditionalCharge extends Model
|
||||
}
|
||||
|
||||
if (!empty($q)) {
|
||||
$query->where(function ($sub) use ($q) {
|
||||
$sub->where('additional_charges.title', 'like', "%$q%")
|
||||
->orWhere('additional_charges.description', 'like', "%$q%")
|
||||
->orWhere('users.firstname', 'like', "%$q%")
|
||||
->orWhere('users.lastname', 'like', "%$q%")
|
||||
->orWhere('invoices.invoice_number', 'like', "%$q%");
|
||||
$term = trim($q);
|
||||
$query->where(function ($w) use ($term) {
|
||||
$w->where('additional_charges.title', 'like', "%{$term}%")
|
||||
->orWhere('additional_charges.description', 'like', "%{$term}%")
|
||||
->orWhere('users.firstname', 'like', "%{$term}%")
|
||||
->orWhere('users.lastname', 'like', "%{$term}%")
|
||||
->orWhere('invoices.invoice_number', 'like', "%{$term}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
return $query->orderByDesc('additional_charges.id');
|
||||
};
|
||||
|
||||
$result = $build($semester)
|
||||
->orderBy('additional_charges.id', 'DESC')
|
||||
->paginate($perPage);
|
||||
// First attempt: year + semester
|
||||
$result = $build($semester)->paginate($perPage);
|
||||
|
||||
if ($fallbackToYear && $result->count() === 0 && $semester !== '') {
|
||||
$result = $build(null)
|
||||
->orderBy('additional_charges.id', 'DESC')
|
||||
->paginate($perPage);
|
||||
// Fallback: if no rows and allowed, retry year-only
|
||||
if ($fallbackToYear && $result->total() === 0 && $semester !== '') {
|
||||
$result = $build(null)->paginate($perPage);
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -7,10 +7,7 @@ use App\Models\BaseModel;
|
||||
class AdminNotificationSubject extends BaseModel
|
||||
{
|
||||
protected $table = 'admin_notification_subjects';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $fillable = [
|
||||
'admin_id',
|
||||
'subject',
|
||||
@@ -18,8 +15,15 @@ class AdminNotificationSubject extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
// CI uses created_at / updated_at
|
||||
public $timestamps = true;
|
||||
protected $casts = [
|
||||
'admin_id' => 'integer',
|
||||
];
|
||||
|
||||
// Optional relationship (if your admins are in users table)
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class Assignment extends BaseModel
|
||||
{
|
||||
protected $table = 'assignments';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = false;
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'due_date',
|
||||
'class_id',
|
||||
'teacher_id',
|
||||
'student_id',
|
||||
];
|
||||
}
|
||||
@@ -7,8 +7,10 @@ use App\Models\BaseModel;
|
||||
class AttendanceCommentTemplate extends BaseModel
|
||||
{
|
||||
protected $table = 'attendance_comment_template';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// CI: useTimestamps = false
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'min_score',
|
||||
'max_score',
|
||||
@@ -18,10 +20,27 @@ class AttendanceCommentTemplate extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function getActiveTemplates(): array
|
||||
protected $casts = [
|
||||
'min_score' => 'integer',
|
||||
'max_score' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Equivalent of CI getActiveTemplates():
|
||||
* is_active=1, order by min_score DESC
|
||||
*/
|
||||
public static function getActiveTemplates()
|
||||
{
|
||||
return $this->where('is_active', 1)
|
||||
->orderBy('min_score', 'DESC')
|
||||
->findAll();
|
||||
return static::query()
|
||||
->where('is_active', 1)
|
||||
->orderByDesc('min_score')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: nice query scope version
|
||||
public function scopeActive($q)
|
||||
{
|
||||
return $q->where('is_active', 1);
|
||||
}
|
||||
}
|
||||
+322
-291
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AttendanceData extends Model
|
||||
class AttendanceData extends BaseModel
|
||||
{
|
||||
protected $table = 'attendance_data';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = true;
|
||||
|
||||
public $timestamps = true; // uses created_at / updated_at
|
||||
|
||||
protected $fillable = [
|
||||
'class_id',
|
||||
'class_section_id',
|
||||
@@ -33,277 +34,268 @@ class AttendanceData extends Model
|
||||
'class_section_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'reported' => 'string',
|
||||
'is_reported' => 'string',
|
||||
'is_notified' => 'string',
|
||||
'date' => 'date',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime'
|
||||
'modified_by' => 'integer',
|
||||
'date' => 'datetime', // ✅ DATETIME column
|
||||
];
|
||||
|
||||
private ?array $reportedColumns = null;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/* =========================
|
||||
* Relationships (optional)
|
||||
* ========================= */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function class()
|
||||
{
|
||||
return $this->belongsTo(ClassModel::class, 'class_id');
|
||||
}
|
||||
|
||||
public function section()
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Methods Converted from CI4
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/** CI => getAttendanceByClass */
|
||||
public function getAttendanceByClass(int $classId, int $classSectionId, string $date)
|
||||
/* =========================
|
||||
* DATETIME helpers
|
||||
* ========================= */
|
||||
private static function dayRange(string $day): array
|
||||
{
|
||||
return self::where('class_id', $classId)
|
||||
$day = substr($day, 0, 10); // YYYY-MM-DD
|
||||
$start = Carbon::parse($day)->startOfDay();
|
||||
$endEx = Carbon::parse($day)->addDay()->startOfDay(); // exclusive
|
||||
return [$start, $endEx];
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Retrieve attendance by class and section for a specific date (DATETIME-safe).
|
||||
*/
|
||||
public static function getAttendanceByClass(int $classId, int $classSectionId, string $date)
|
||||
{
|
||||
[$start, $endEx] = static::dayRange($date);
|
||||
|
||||
return static::query()
|
||||
->where('class_id', $classId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('date', $date)
|
||||
->get()
|
||||
->toArray();
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->get();
|
||||
}
|
||||
|
||||
/** CI => unreportedTermRows */
|
||||
public function unreportedTermRows(array $studentIds, string $schoolYear, ?string $semester): array
|
||||
/**
|
||||
* Update attendance record by PK.
|
||||
*/
|
||||
public static function updateAttendance(int $attendanceId, array $data): bool
|
||||
{
|
||||
$qb = DB::table($this->table)
|
||||
->select(
|
||||
'id',
|
||||
'student_id',
|
||||
'class_id',
|
||||
'class_section_id',
|
||||
'date',
|
||||
'status',
|
||||
'reason',
|
||||
'school_year',
|
||||
'semester',
|
||||
'is_reported'
|
||||
)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$qb->where('semester', $semester);
|
||||
}
|
||||
if (!empty($studentIds)) {
|
||||
$qb->whereIn('student_id', $studentIds);
|
||||
}
|
||||
|
||||
$this->applyUnreportedFilter($qb);
|
||||
|
||||
return $qb->orderBy('date', 'DESC')
|
||||
->get()
|
||||
->map(static function ($row) {
|
||||
return (array) $row;
|
||||
})
|
||||
->all();
|
||||
$affected = static::query()->whereKey($attendanceId)->update($data);
|
||||
return $affected >= 0;
|
||||
}
|
||||
|
||||
/** CI => updateAttendance */
|
||||
public function updateAttendance(int $id, array $data)
|
||||
/**
|
||||
* Retrieve attendance for a specific student, class section, and date (DATETIME-safe).
|
||||
*/
|
||||
public static function getAttendance(int $studentId, int $classSectionId, string $date): ?self
|
||||
{
|
||||
return self::where('id', $id)->update($data) > 0;
|
||||
[$start, $endEx] = static::dayRange($date);
|
||||
|
||||
return static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->first();
|
||||
}
|
||||
|
||||
/** CI => getAttendance */
|
||||
public function getAttendance(int $studentId, int $classSectionId, string $date)
|
||||
/**
|
||||
* Retrieve reported attendance for a specific date or range (DATETIME-safe).
|
||||
*/
|
||||
public static function getReportedAttendance(string $startDate, ?string $endDate = null)
|
||||
{
|
||||
$row = self::where([
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date
|
||||
])->first();
|
||||
$q = static::query();
|
||||
static::applyReportedFilter($q);
|
||||
|
||||
return $row ? $row->toArray() : null;
|
||||
}
|
||||
|
||||
/** CI => getReportedAttendance */
|
||||
public function getReportedAttendance(string $startDate, ?string $endDate = null)
|
||||
{
|
||||
$query = self::where(function ($q) {
|
||||
$q->where('is_reported', 1)
|
||||
->orWhere('is_reported', 'yes');
|
||||
});
|
||||
[$start, ] = static::dayRange($startDate);
|
||||
|
||||
if ($endDate) {
|
||||
$query->whereBetween('date', [$startDate, $endDate]);
|
||||
[, $endEx] = static::dayRange($endDate); // end exclusive of next day
|
||||
$q->where('date', '>=', $start)->where('date', '<', $endEx);
|
||||
} else {
|
||||
$query->where('date', $startDate);
|
||||
[, $endEx] = static::dayRange($startDate);
|
||||
$q->where('date', '>=', $start)->where('date', '<', $endEx);
|
||||
}
|
||||
|
||||
return $query->get()->toArray();
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/** CI => getTodayAbsentees */
|
||||
public function getTodayAbsentees(?int $classSectionId = null, ?string $schoolYear = null, ?string $semester = null)
|
||||
public static function getTodayAbsentees(?int $classSectionId = null, ?string $schoolYear = null, ?string $semester = null)
|
||||
{
|
||||
$query = self::where('status', 'Absent')
|
||||
->where('date', now()->toDateString());
|
||||
[$start, $endEx] = [now()->startOfDay(), now()->addDay()->startOfDay()];
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$query->where('class_section_id', $classSectionId);
|
||||
}
|
||||
if ($schoolYear !== null) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== null) {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
$q = static::query()
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->where(function ($w) {
|
||||
$w->whereIn(DB::raw("UPPER(TRIM(status))"), ['ABSENT', 'ABS', 'A'])
|
||||
->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'");
|
||||
});
|
||||
|
||||
return $query->get()->toArray();
|
||||
if ($classSectionId !== null) $q->where('class_section_id', $classSectionId);
|
||||
if ($schoolYear !== null) $q->where('school_year', $schoolYear);
|
||||
if ($semester !== null) $q->where('semester', $semester);
|
||||
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/** CI => getTodayLates */
|
||||
public function getTodayLates(?int $classSectionId = null, ?string $schoolYear = null, ?string $semester = null)
|
||||
public static function getTodayLates(?int $classSectionId = null, ?string $schoolYear = null, ?string $semester = null)
|
||||
{
|
||||
$query = self::where('status', 'Late')
|
||||
->where('date', now()->toDateString());
|
||||
[$start, $endEx] = [now()->startOfDay(), now()->addDay()->startOfDay()];
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$query->where('class_section_id', $classSectionId);
|
||||
}
|
||||
if ($schoolYear !== null) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
}
|
||||
if ($semester !== null) {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
$q = static::query()
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->where(function ($w) {
|
||||
$w->whereIn(DB::raw("UPPER(TRIM(status))"), ['LATE', 'L'])
|
||||
->orWhereRaw("UPPER(TRIM(status)) LIKE 'LATE%'");
|
||||
});
|
||||
|
||||
return $query->get()->toArray();
|
||||
if ($classSectionId !== null) $q->where('class_section_id', $classSectionId);
|
||||
if ($schoolYear !== null) $q->where('school_year', $schoolYear);
|
||||
if ($semester !== null) $q->where('semester', $semester);
|
||||
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/** CI => getAbsencesByClassSection */
|
||||
public function getAbsencesByClassSection($classSectionId, $schoolYear, $semester)
|
||||
public static function getAbsencesByClassSection(int $classSectionId, string $schoolYear, string $semester)
|
||||
{
|
||||
return self::where('status', 'absent')
|
||||
return static::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->orderBy('date', 'DESC')
|
||||
->get()
|
||||
->toArray();
|
||||
->where(function ($w) {
|
||||
$w->whereIn(DB::raw("UPPER(TRIM(status))"), ['ABSENT', 'ABS', 'A'])
|
||||
->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'");
|
||||
})
|
||||
->orderByDesc('date')
|
||||
->get();
|
||||
}
|
||||
|
||||
/** CI => getStudentAttendance */
|
||||
public function getStudentAttendance(
|
||||
/**
|
||||
* Fetch attendance rows for a student, optionally filtered by date range.
|
||||
* Note: Uses inclusive date range by day using datetime bounds.
|
||||
*/
|
||||
public static function getStudentAttendance(
|
||||
int $studentId,
|
||||
?string $startDate = null,
|
||||
?string $endDate = null,
|
||||
?string $endDate = null,
|
||||
?string $schoolYear = null,
|
||||
?string $semester = null
|
||||
): array {
|
||||
$query = self::where('student_id', $studentId);
|
||||
?string $semester = null
|
||||
) {
|
||||
$q = static::query()->where('student_id', $studentId);
|
||||
|
||||
if ($startDate) {
|
||||
$query->where('date', '>=', $startDate);
|
||||
if (!empty($startDate)) {
|
||||
[$start, ] = static::dayRange($startDate);
|
||||
$q->where('date', '>=', $start);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->where('date', '<=', $endDate);
|
||||
}
|
||||
if ($schoolYear) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
}
|
||||
if ($semester) {
|
||||
$query->where('semester', $semester);
|
||||
if (!empty($endDate)) {
|
||||
[, $endEx] = static::dayRange($endDate);
|
||||
$q->where('date', '<', $endEx);
|
||||
}
|
||||
if (!empty($schoolYear)) $q->where('school_year', $schoolYear);
|
||||
if (!empty($semester)) $q->where('semester', $semester);
|
||||
|
||||
return $query->orderBy('date', 'DESC')->get()->toArray();
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
/** CI => getStudentAttendanceOnDate */
|
||||
public function getStudentAttendanceOnDate(int $studentId, string $date): ?array
|
||||
/**
|
||||
* Convenience: attendance for an exact date (DATETIME-safe).
|
||||
*/
|
||||
public static function getStudentAttendanceOnDate(int $studentId, string $date): ?self
|
||||
{
|
||||
$row = self::where('student_id', $studentId)
|
||||
->where('date', $date)
|
||||
[$start, $endEx] = static::dayRange($date);
|
||||
|
||||
return static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->first();
|
||||
|
||||
return $row ? $row->toArray() : null;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CI Complex Function => getUnreportedAbsencesAndLates()
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function getUnreportedAbsencesAndLates(?string $schoolYear = null, ?string $semester = null): array
|
||||
/**
|
||||
* Unreported rows for students in term.
|
||||
*/
|
||||
public static function unreportedTermRows(array $studentIds, string $schoolYear, ?string $semester)
|
||||
{
|
||||
$query = DB::table($this->table)
|
||||
->select('id', 'student_id', 'class_id', 'class_section_id', 'date', 'status', 'reason', 'school_year', 'semester', 'is_reported')
|
||||
->whereNotNull('student_id');
|
||||
$q = static::query()
|
||||
->select(['id','student_id','class_id','class_section_id','date','status','reason','school_year','semester','is_reported'])
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
$this->applyUnreportedFilter($query);
|
||||
|
||||
$query->where(function ($q) {
|
||||
$q->where('is_notified', 0)
|
||||
->orWhere('is_notified', '0')
|
||||
->orWhereNull('is_notified')
|
||||
->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) != 'yes'");
|
||||
});
|
||||
|
||||
if ($schoolYear) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
if (!empty($studentIds)) {
|
||||
$q->whereIn('student_id', array_map('intval', $studentIds));
|
||||
}
|
||||
|
||||
if ($semester) {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
static::applyUnreportedFilter($q);
|
||||
|
||||
// Status filter
|
||||
$query->where(function ($q) {
|
||||
$q->whereIn('status', ['ABS', 'ABSENT', 'LATE', 'A', 'L'])
|
||||
->orWhere('status', 'like', 'ABS%')
|
||||
->orWhere('status', 'like', 'LATE%');
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT-REPORTED absences/lates for ALL students in optional term filters.
|
||||
* Returns grouped array: [student_id => ['absences'=>[], 'lates'=>[], 'counts'=>...]]
|
||||
*/
|
||||
public static function getUnreportedAbsencesAndLates(?string $schoolYear = null, ?string $semester = null): array
|
||||
{
|
||||
$q = static::query()
|
||||
->select(['id','student_id','class_id','class_section_id','date','status','reason','school_year','semester','is_reported']);
|
||||
|
||||
static::applyUnreportedFilter($q);
|
||||
|
||||
// Not notified yet: supports 0/'0'/NULL and string enums like 'yes'
|
||||
$q->where(function ($w) {
|
||||
$w->whereNull('is_notified')
|
||||
->orWhere('is_notified', 0)
|
||||
->orWhere('is_notified', '0')
|
||||
->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) != 'yes'");
|
||||
});
|
||||
|
||||
$rows = $query->orderBy('student_id', 'ASC')
|
||||
->orderBy('date', 'DESC')
|
||||
->get()
|
||||
->toArray();
|
||||
if (!empty($schoolYear)) $q->where('school_year', $schoolYear);
|
||||
if (!empty($semester)) $q->where('semester', $semester);
|
||||
|
||||
// Status filter: exact + prefixes (ABS_3, LATE_2, etc.)
|
||||
$q->where(function ($w) {
|
||||
$w->whereIn(DB::raw("UPPER(TRIM(status))"), ['ABS','ABSENT','LATE','A','L'])
|
||||
->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'")
|
||||
->orWhereRaw("UPPER(TRIM(status)) LIKE 'LATE%'");
|
||||
});
|
||||
|
||||
$rows = $q->orderBy('student_id')->orderByDesc('date')->get()->toArray();
|
||||
|
||||
// Now group output like original CI function
|
||||
$out = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$sid = (int) $r->student_id;
|
||||
$sid = (int) ($r['student_id'] ?? 0);
|
||||
|
||||
if (!isset($out[$sid])) {
|
||||
$out[$sid] = [
|
||||
'absences' => [],
|
||||
'lates' => [],
|
||||
'counts' => ['absences' => 0, 'lates' => 0, 'total' => 0],
|
||||
'lates' => [],
|
||||
'counts' => ['absences' => 0, 'lates' => 0, 'total' => 0],
|
||||
];
|
||||
}
|
||||
|
||||
$status = strtoupper($r->status);
|
||||
$isAbs = $status === 'ABS' || $status === 'ABSENT' || $status === 'A' || str_starts_with($status, 'ABS');
|
||||
$isLate = $status === 'LATE' || $status === 'L' || str_starts_with($status, 'LATE');
|
||||
$st = strtoupper(trim((string)($r['status'] ?? '')));
|
||||
$isAbs = ($st === 'ABS' || $st === 'ABSENT' || $st === 'A' || str_starts_with($st, 'ABS'));
|
||||
$isLate = ($st === 'LATE' || $st === 'L' || str_starts_with($st, 'LATE'));
|
||||
|
||||
$rec = [
|
||||
'id' => $r->id,
|
||||
'date' => $r->date,
|
||||
'status' => $r->status,
|
||||
'reason' => $r->reason,
|
||||
'class_id' => $r->class_id,
|
||||
'class_section_id' => $r->class_section_id,
|
||||
'school_year' => $r->school_year,
|
||||
'semester' => $r->semester,
|
||||
'id' => $r['id'] ?? null,
|
||||
'date' => $r['date'] ?? null,
|
||||
'status' => $r['status'] ?? null,
|
||||
'reason' => $r['reason'] ?? null,
|
||||
'class_id' => $r['class_id'] ?? null,
|
||||
'class_section_id' => $r['class_section_id'] ?? null,
|
||||
'school_year' => $r['school_year'] ?? null,
|
||||
'semester' => $r['semester'] ?? null,
|
||||
];
|
||||
|
||||
if ($isAbs) {
|
||||
@@ -321,76 +313,80 @@ class AttendanceData extends Model
|
||||
return $out;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CI => markRuleAsNotifiedData()
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/**
|
||||
* Mark rule as notified for a student on a specific day (DATETIME-safe).
|
||||
*/
|
||||
public static function markRuleAsNotifiedData(
|
||||
int $studentId,
|
||||
string $date,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
[$start, $endEx] = static::dayRange($date);
|
||||
|
||||
public function markRuleAsNotifiedData(int $studentId, string $date, ?string $semester = null, ?string $schoolYear = null): bool
|
||||
{
|
||||
$day = substr($date, 0, 10);
|
||||
$start = $day . ' 00:00:00';
|
||||
$endEx = date('Y-m-d', strtotime($day . ' +1 day')) . ' 00:00:00';
|
||||
$query = DB::table($this->table)
|
||||
$q = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('date >=', $start)
|
||||
->where('date <', $endEx);
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx);
|
||||
|
||||
if (!empty($semester)) {
|
||||
$query->where('semester', $semester);
|
||||
}
|
||||
if (!empty($semester)) $q->where('semester', $semester);
|
||||
if (!empty($schoolYear)) $q->where('school_year', $schoolYear);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $query->update([
|
||||
$affected = $q->update([
|
||||
'is_notified' => 'yes',
|
||||
'updated_at' => utc_now(),
|
||||
]) > 0;
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected >= 0;
|
||||
}
|
||||
|
||||
public function markReportedAndNotified(int $studentId, array $dates, ?string $semester = null, ?string $schoolYear = null): bool
|
||||
/**
|
||||
* Mark specific attendance dates as reported + notified for a student (DATETIME index-friendly).
|
||||
*/
|
||||
public static function markReportedAndNotified(int $studentId, array $dates, ?string $semester = null, ?string $schoolYear = null): bool
|
||||
{
|
||||
$dates = array_values(array_unique(array_filter($dates, static function ($d) {
|
||||
return is_string($d) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
||||
})));
|
||||
if (empty($dates)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$builder = DB::table($this->table)
|
||||
if (empty($dates)) return true;
|
||||
|
||||
$q = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->whereIn('date', $dates)
|
||||
->whereRaw("LOWER(TRIM(status)) IN ('absent','late')");
|
||||
|
||||
if (!empty($semester)) {
|
||||
$builder->where('semester', $semester);
|
||||
}
|
||||
if (!empty($schoolYear)) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
}
|
||||
if (!empty($semester)) $q->where('semester', $semester);
|
||||
if (!empty($schoolYear)) $q->where('school_year', $schoolYear);
|
||||
|
||||
$cols = $this->reportedColumns();
|
||||
$updates = [
|
||||
'updated_at' => utc_now(),
|
||||
// OR day ranges instead of DATE(date)
|
||||
$q->where(function ($w) use ($dates) {
|
||||
foreach ($dates as $d) {
|
||||
[$start, $endEx] = static::dayRange($d);
|
||||
$w->orWhere(function ($x) use ($start, $endEx) {
|
||||
$x->where('date', '>=', $start)->where('date', '<', $endEx);
|
||||
});
|
||||
$w->orWhere('date', $d);
|
||||
}
|
||||
});
|
||||
|
||||
$payload = [
|
||||
'updated_at' => now(),
|
||||
'is_notified' => 'yes',
|
||||
];
|
||||
|
||||
if (!empty($cols['is_reported'])) {
|
||||
$updates['is_reported'] = 'yes';
|
||||
}
|
||||
if (!empty($cols['reported'])) {
|
||||
$updates['reported'] = 1;
|
||||
}
|
||||
if (Schema::hasColumn($this->table, 'is_notified')) {
|
||||
$updates['is_notified'] = 'yes';
|
||||
}
|
||||
$cols = static::reportedColumns();
|
||||
if (!empty($cols['is_reported'])) $payload['is_reported'] = 'yes';
|
||||
if (!empty($cols['reported'])) $payload['reported'] = 1;
|
||||
|
||||
return $builder->update($updates) > 0;
|
||||
$affected = $q->update($payload);
|
||||
|
||||
return $affected >= 0;
|
||||
}
|
||||
|
||||
public function getRecentUnreportedDates(
|
||||
/**
|
||||
* Fetch unreported dates for a student within N days for the given status set (DATETIME-safe).
|
||||
*/
|
||||
public static function getRecentUnreportedDates(
|
||||
int $studentId,
|
||||
array $statuses,
|
||||
?string $incidentDate = null,
|
||||
@@ -398,64 +394,99 @@ class AttendanceData extends Model
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
if ($studentId <= 0 || empty($statuses)) {
|
||||
return [];
|
||||
}
|
||||
$statuses = array_map('strtolower', $statuses);
|
||||
$day = $incidentDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $incidentDate)
|
||||
? $incidentDate
|
||||
: date('Y-m-d');
|
||||
$start = date('Y-m-d', strtotime($day . ' -' . max(0, $lookbackDays) . ' days'));
|
||||
if ($studentId <= 0 || empty($statuses)) return [];
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($statuses), '?'));
|
||||
$qb = DB::table($this->table)
|
||||
$statuses = array_map('strtolower', $statuses);
|
||||
|
||||
$day = ($incidentDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $incidentDate))
|
||||
? $incidentDate
|
||||
: now()->toDateString();
|
||||
|
||||
$startDay = Carbon::parse($day)->subDays(max(0, $lookbackDays))->toDateString();
|
||||
[$start, ] = static::dayRange($startDay);
|
||||
[, $endEx] = static::dayRange($day);
|
||||
|
||||
$q = static::query()
|
||||
->select('date')
|
||||
->where('student_id', $studentId)
|
||||
->where('date >=', $start)
|
||||
->where('date <=', $day)
|
||||
->whereRaw("LOWER(TRIM(status)) IN ({$placeholders})", $statuses);
|
||||
$this->applyUnreportedFilter($qb);
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->whereIn(DB::raw("LOWER(TRIM(status))"), $statuses);
|
||||
|
||||
if (!empty($semester)) {
|
||||
$qb->where('semester', $semester);
|
||||
}
|
||||
if (!empty($schoolYear)) {
|
||||
$qb->where('school_year', $schoolYear);
|
||||
}
|
||||
static::applyUnreportedFilter($q);
|
||||
|
||||
$rows = $qb->orderBy('date', 'DESC')->get()->toArray();
|
||||
return array_values(array_unique(array_map(static function ($r) {
|
||||
return substr((string) ($r->date ?? ''), 0, 10);
|
||||
if (!empty($semester)) $q->where('semester', $semester);
|
||||
if (!empty($schoolYear)) $q->where('school_year', $schoolYear);
|
||||
|
||||
$rows = $q->orderByDesc('date')->pluck('date')->all();
|
||||
|
||||
return array_values(array_unique(array_map(static function ($d) {
|
||||
return substr((string)$d, 0, 10); // YYYY-MM-DD
|
||||
}, $rows)));
|
||||
}
|
||||
|
||||
private function reportedColumns(): array
|
||||
{
|
||||
if ($this->reportedColumns !== null) {
|
||||
return $this->reportedColumns;
|
||||
}
|
||||
/* =========================
|
||||
* Filters / schema helpers
|
||||
* ========================= */
|
||||
|
||||
$this->reportedColumns = [
|
||||
'is_reported' => Schema::hasColumn($this->table, 'is_reported'),
|
||||
'reported' => Schema::hasColumn($this->table, 'reported'),
|
||||
private static function reportedColumns(): array
|
||||
{
|
||||
static $cache = null;
|
||||
if ($cache !== null) return $cache;
|
||||
|
||||
$table = (new static)->getTable();
|
||||
|
||||
$cache = [
|
||||
'is_reported' => static::columnExists($table, 'is_reported'),
|
||||
'reported' => static::columnExists($table, 'reported'),
|
||||
];
|
||||
|
||||
return $this->reportedColumns;
|
||||
return $cache;
|
||||
}
|
||||
|
||||
private function applyUnreportedFilter($qb): void
|
||||
private static function columnExists(string $table, string $column): bool
|
||||
{
|
||||
$cols = $this->reportedColumns();
|
||||
if (!empty($cols['is_reported'])) {
|
||||
$qb->where(function ($q) {
|
||||
$q->where('is_reported', 'no')
|
||||
->orWhere('is_reported', 0)
|
||||
->orWhere('is_reported', '0')
|
||||
->orWhereNull('is_reported');
|
||||
});
|
||||
}
|
||||
if (!empty($cols['reported'])) {
|
||||
$qb->whereRaw("LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on')");
|
||||
try {
|
||||
return DB::connection()
|
||||
->getSchemaBuilder()
|
||||
->hasColumn($table, $column);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not-reported filter:
|
||||
* - is_reported = 'no' OR 0 OR NULL (if column exists)
|
||||
* - legacy 'reported' column: treat values yes/1/true/on as reported, so exclude those
|
||||
*/
|
||||
private static function applyUnreportedFilter($q): void
|
||||
{
|
||||
$cols = static::reportedColumns();
|
||||
|
||||
if (!empty($cols['is_reported'])) {
|
||||
$q->where(function ($w) {
|
||||
$w->whereNull('is_reported')
|
||||
->orWhere('is_reported', 0)
|
||||
->orWhere('is_reported', '0')
|
||||
->orWhereRaw("LOWER(TRIM(COALESCE(is_reported, ''))) = 'no'");
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($cols['reported'])) {
|
||||
$q->whereRaw("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reported filter best-effort (used by getReportedAttendance()).
|
||||
*/
|
||||
private static function applyReportedFilter($q): void
|
||||
{
|
||||
$q->where(function ($w) {
|
||||
$w->where('is_reported', 1)
|
||||
->orWhere('is_reported', '1')
|
||||
->orWhereRaw("LOWER(TRIM(COALESCE(is_reported, ''))) = 'yes'");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Database\QueryException;
|
||||
|
||||
class AttendanceDay extends Model
|
||||
class AttendanceDay extends BaseModel
|
||||
{
|
||||
protected $table = 'attendance_day'; // Because you store created_at/updated_at but CI didn't auto-manage them
|
||||
protected $table = 'attendance_day';
|
||||
|
||||
// ✅ You are storing created_at / updated_at manually in CI (timestamps off there)
|
||||
// User didn't explicitly ask to auto-manage here, so we keep it OFF to match CI.
|
||||
// If you want auto-manage, tell me and I’ll flip it.
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'class_section_id',
|
||||
'date',
|
||||
@@ -30,27 +35,29 @@ class AttendanceDay extends Model
|
||||
|
||||
protected $casts = [
|
||||
'class_section_id' => 'integer',
|
||||
'date' => 'date:Y-m-d',
|
||||
'submitted_by' => 'integer',
|
||||
'published_by' => 'integer',
|
||||
'reopened_by' => 'integer',
|
||||
|
||||
// If these are DATETIME columns:
|
||||
'submitted_at' => 'datetime',
|
||||
'published_at' => 'datetime',
|
||||
'auto_publish_at' => 'datetime',
|
||||
'reopened_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
|
||||
// If date is stored as DATE string; if it's DATETIME, change to 'datetime'
|
||||
'date' => 'date',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================
|
||||
*/
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Legacy helper kept for compatibility.
|
||||
* True iff this (section, date, term) is hard-locked for everyone.
|
||||
* Treats 'published' (new) and 'finalized' (old) as finalized.
|
||||
* Legacy helper: True iff (section, date, term) is hard-locked for everyone.
|
||||
* Treats 'published' and legacy 'finalized' as finalized.
|
||||
*/
|
||||
public static function isFinalized(
|
||||
int $classSectionId,
|
||||
@@ -61,10 +68,10 @@ class AttendanceDay extends Model
|
||||
$q = static::query()
|
||||
->select('id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->whereDate('date', $date)
|
||||
->where(function (Builder $w) {
|
||||
->where('date', $date)
|
||||
->where(function ($w) {
|
||||
$w->where('status', 'published')
|
||||
->orWhere('status', 'finalized'); // tolerate legacy data
|
||||
->orWhere('status', 'finalized'); // legacy
|
||||
});
|
||||
|
||||
if ($semester !== null) $q->where('semester', $semester);
|
||||
@@ -74,7 +81,7 @@ class AttendanceDay extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a day row by (section, date, term). Returns model|null.
|
||||
* Find day row by (section, date, term).
|
||||
*/
|
||||
public static function findBySectionDate(
|
||||
int $classSectionId,
|
||||
@@ -83,17 +90,16 @@ class AttendanceDay extends Model
|
||||
?string $schoolYear
|
||||
): ?self {
|
||||
return static::query()
|
||||
->where([
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
])->first();
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the draft row for (section,date,term), creating it if missing.
|
||||
* Relies on the unique key (class_section_id, date, semester, school_year) to prevent duplicates.
|
||||
* Get draft row for (section,date,term), create it if missing.
|
||||
* Requires unique index: (class_section_id, date, semester, school_year)
|
||||
*/
|
||||
public static function getOrCreateDraft(
|
||||
int $classSectionId,
|
||||
@@ -102,97 +108,98 @@ class AttendanceDay extends Model
|
||||
?string $schoolYear,
|
||||
int $userId = 0
|
||||
): self {
|
||||
$now = CarbonImmutable::now('UTC');
|
||||
$row = static::findBySectionDate($classSectionId, $date, $semester, $schoolYear);
|
||||
if ($row) return $row;
|
||||
|
||||
// firstOrCreate is already race-safe with a DB unique index:
|
||||
// if two requests race, one insert wins; the other re-queries.
|
||||
return static::query()->firstOrCreate(
|
||||
[
|
||||
$now = now();
|
||||
|
||||
try {
|
||||
return static::create([
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => $date,
|
||||
'status' => 'draft',
|
||||
'submitted_by' => null,
|
||||
'submitted_at' => null,
|
||||
'published_by' => null,
|
||||
'published_at' => null,
|
||||
'auto_publish_at' => null,
|
||||
'reopened_by' => null,
|
||||
'reopened_at' => null,
|
||||
'reopen_reason' => null,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
],
|
||||
[
|
||||
'status' => 'draft',
|
||||
'submitted_by' => null,
|
||||
'submitted_at' => null,
|
||||
'published_by' => null,
|
||||
'published_at' => null,
|
||||
'auto_publish_at' => null,
|
||||
'reopened_by' => null,
|
||||
'reopened_at' => null,
|
||||
'reopen_reason' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]
|
||||
);
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
} catch (QueryException $e) {
|
||||
// Race: someone inserted same unique key.
|
||||
$row = static::findBySectionDate($classSectionId, $date, $semester, $schoolYear);
|
||||
if ($row) return $row;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED: Prior code used "finalize" to lock immediately.
|
||||
* In the new workflow, "finalize" means "teacher submit".
|
||||
* DEPRECATED legacy finalize => submit (teacher submit).
|
||||
*/
|
||||
public function finalize(int $userId): bool
|
||||
public static function finalize(int $id, int $userId): bool
|
||||
{
|
||||
return $this->submit($userId, null);
|
||||
return static::submit($id, $userId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teacher submit: status => submitted (teacher locked, admin editable).
|
||||
* Optionally set auto_publish_at.
|
||||
*/
|
||||
public function submit(int $userId, ?string $autoPublishAt = null): bool
|
||||
public static function submit(int $id, int $userId, ?string $autoPublishAt = null): bool
|
||||
{
|
||||
$now = CarbonImmutable::now('UTC');
|
||||
$now = now();
|
||||
|
||||
$this->fill([
|
||||
'status' => 'submitted',
|
||||
'submitted_by' => $userId,
|
||||
'submitted_at' => $now,
|
||||
'auto_publish_at' => $autoPublishAt,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$payload = [
|
||||
'status' => 'submitted',
|
||||
'submitted_by' => $userId,
|
||||
'submitted_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
return $this->save();
|
||||
if ($autoPublishAt !== null) {
|
||||
$payload['auto_publish_at'] = $autoPublishAt;
|
||||
}
|
||||
|
||||
return static::query()->whereKey($id)->update($payload) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin publish (hard lock): status => published.
|
||||
*/
|
||||
public function publish(int $userId): bool
|
||||
public static function publish(int $id, int $userId): bool
|
||||
{
|
||||
$now = CarbonImmutable::now('UTC');
|
||||
$now = now();
|
||||
|
||||
$this->fill([
|
||||
return static::query()->whereKey($id)->update([
|
||||
'status' => 'published',
|
||||
'published_by' => $userId,
|
||||
'published_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
return $this->save();
|
||||
]) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin reopen a submitted/published day back to 'draft' (default) or 'submitted'.
|
||||
* Admin reopen a day back to 'draft' (default) or 'submitted'.
|
||||
*/
|
||||
public function reopen(int $userId, string $toStatus = 'draft', ?string $reason = null): bool
|
||||
public static function reopen(int $id, int $userId, string $toStatus = 'draft', ?string $reason = null): bool
|
||||
{
|
||||
if (!in_array($toStatus, ['draft', 'submitted'], true)) {
|
||||
throw new \InvalidArgumentException('toStatus must be "draft" or "submitted".');
|
||||
}
|
||||
|
||||
$now = CarbonImmutable::now('UTC');
|
||||
$now = now();
|
||||
|
||||
$this->fill([
|
||||
return static::query()->whereKey($id)->update([
|
||||
'status' => $toStatus,
|
||||
'reopened_by' => $userId,
|
||||
'reopened_at' => $now,
|
||||
'reopen_reason' => $reason,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
return $this->save();
|
||||
]) >= 0;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class AttendanceEmailTemplate extends Model
|
||||
class AttendanceEmailTemplate extends BaseModel
|
||||
{
|
||||
protected $table = 'email_templates';
|
||||
|
||||
// ✅ Laravel will auto-manage created_at / updated_at
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'variant',
|
||||
@@ -16,35 +20,32 @@ class AttendanceEmailTemplate extends Model
|
||||
'updated_by',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'updated_by' => 'integer',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch an active template by code and variant, falling back to default.
|
||||
* Fetch a template by code and variant, falling back to 'default' if needed.
|
||||
* Active only.
|
||||
*/
|
||||
public function getTemplate(string $code, string $variant = 'default'): ?array
|
||||
public static function getTemplate(string $code, string $variant = 'default'): ?self
|
||||
{
|
||||
$row = self::query()
|
||||
$row = static::query()
|
||||
->where('code', $code)
|
||||
->where('variant', $variant)
|
||||
->where('is_active', true)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if ($row) {
|
||||
return $row->toArray();
|
||||
return $row;
|
||||
}
|
||||
|
||||
$row = self::query()
|
||||
return static::query()
|
||||
->where('code', $code)
|
||||
->where('variant', 'default')
|
||||
->where('is_active', true)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
return $row ? $row->toArray() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceRecord extends Model
|
||||
class AttendanceRecord extends BaseModel
|
||||
{
|
||||
protected $table = 'attendance_record';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// Enable timestamps using custom CI fields
|
||||
// ✅ CI uses created_at / updated_at
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $fillable = [
|
||||
'class_section_id',
|
||||
'student_id',
|
||||
@@ -29,25 +28,19 @@ class AttendanceRecord extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'class_section_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'total_absence' => 'integer',
|
||||
'total_late' => 'integer',
|
||||
'total_presence' => 'integer',
|
||||
'total_attendance' => 'integer',
|
||||
'modified_by' => 'integer',
|
||||
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'class_section_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'total_absence' => 'integer',
|
||||
'total_late' => 'integer',
|
||||
'total_presence' => 'integer',
|
||||
'total_attendance' => 'integer',
|
||||
'modified_by' => 'integer',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/* =========================
|
||||
* Relationships (optional)
|
||||
* ========================= */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
@@ -58,97 +51,92 @@ class AttendanceRecord extends Model
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
public function modifier()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'modified_by');
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Converted CodeIgniter Methods
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Equivalent to CI getAttendanceRecordByClass()
|
||||
* Get attendance records by class section, semester, and school year.
|
||||
*/
|
||||
public function getAttendanceRecordByClass($classSectionId, $semester, $schoolYear)
|
||||
public static function getAttendanceRecordByClass(int $classSectionId, string $semester, string $schoolYear)
|
||||
{
|
||||
return self::where('class_section_id', $classSectionId)
|
||||
return static::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get()
|
||||
->toArray();
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to CI getAttendanceRecord()
|
||||
* Get attendance record for a specific student in a class section.
|
||||
*/
|
||||
public function getAttendanceRecord($studentId, $classSectionId, $semester, $schoolYear)
|
||||
public static function getAttendanceRecord(int $studentId, int $classSectionId, string $semester, string $schoolYear): ?self
|
||||
{
|
||||
return self::where([
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
])->first()?->toArray();
|
||||
return static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to CI updateAttendanceRecord()
|
||||
* Update an existing attendance record by PK.
|
||||
*/
|
||||
public function updateAttendanceRecord($attendanceId, $data)
|
||||
public static function updateAttendanceRecord(int $attendanceId, array $data): bool
|
||||
{
|
||||
return self::where('id', $attendanceId)->update($data) > 0;
|
||||
$affected = static::query()->whereKey($attendanceId)->update($data);
|
||||
return $affected >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel-optimized version of CI incrementAttendanceCounters()
|
||||
* Uses atomic SQL increments.
|
||||
* Increment attendance counters for a student (atomic, avoids race conditions).
|
||||
*
|
||||
* $increments example: ['total_absence' => 1, 'total_presence' => 0]
|
||||
*/
|
||||
public function incrementAttendanceCounters(
|
||||
public static function incrementAttendanceCounters(
|
||||
int $studentId,
|
||||
int $classSectionId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
array $increments
|
||||
): bool {
|
||||
$record = self::where([
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
])->first();
|
||||
$allowed = ['total_absence','total_late','total_presence','total_attendance'];
|
||||
$inc = array_intersect_key($increments, array_flip($allowed));
|
||||
|
||||
if (!$record) {
|
||||
return false;
|
||||
if (empty($inc)) return true;
|
||||
|
||||
// Build atomic update: field = field + X
|
||||
$update = [];
|
||||
foreach ($inc as $field => $value) {
|
||||
$value = (int) $value;
|
||||
if ($value === 0) continue;
|
||||
$update[$field] = DB::raw("COALESCE($field,0) + {$value}");
|
||||
}
|
||||
|
||||
foreach ($increments as $field => $value) {
|
||||
if (in_array($field, [
|
||||
'total_absence',
|
||||
'total_late',
|
||||
'total_presence',
|
||||
'total_attendance'
|
||||
])) {
|
||||
$record->increment($field, $value);
|
||||
}
|
||||
}
|
||||
if (empty($update)) return true;
|
||||
|
||||
$record->touch(); // update updated_at
|
||||
return true;
|
||||
$affected = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->update($update);
|
||||
|
||||
// If no record exists, return false (same idea as your CI code)
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to CI getTotalAbsences()
|
||||
* Total absences for a student in a term.
|
||||
* (Equivalent result, but more efficient than summing in PHP.)
|
||||
*/
|
||||
public function getTotalAbsences($studentId, $semester, $schoolYear): int
|
||||
public static function getTotalAbsences(int $studentId, string $semester, string $schoolYear): int
|
||||
{
|
||||
$record = self::where('student_id', $studentId)
|
||||
return (int) static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
return $record?->total_absence ?? 0;
|
||||
->sum('total_absence');
|
||||
}
|
||||
}
|
||||
}
|
||||
+342
-240
@@ -4,15 +4,15 @@ namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceTracking extends BaseModel
|
||||
{
|
||||
protected $table = 'attendance_tracking';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'date',
|
||||
@@ -28,272 +28,313 @@ class AttendanceTracking extends BaseModel
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'is_reported' => 'integer',
|
||||
'is_notified' => 'integer',
|
||||
'notif_counter' => 'integer',
|
||||
'date' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'student_id' => 'integer',
|
||||
'date' => 'datetime', // CI dateFormat = datetime
|
||||
'is_reported' => 'integer',
|
||||
'is_notified' => 'integer',
|
||||
'notif_counter' => 'integer',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relationships
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
/* ----------------------- Helpers ----------------------- */
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| INTERNAL HELPERS (converted from CI)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
private function normalizeDateTime(string $date): string
|
||||
/** Normalize Y-m-d (or Y-m-d H:i:s) into a full 'Y-m-d H:i:s' at midnight if time missing. */
|
||||
private static function normalizeDateTime(string $date): string
|
||||
{
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $date)) {
|
||||
return $date;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
return $date . ' 00:00:00';
|
||||
}
|
||||
|
||||
return Carbon::parse($date)->toDateTimeString();
|
||||
try {
|
||||
return Carbon::parse($date, 'UTC')->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
return Carbon::now('UTC')->startOfDay()->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
|
||||
private function applyTermFilters($query, ?string $semester, ?string $schoolYear)
|
||||
/** Apply semester/schoolYear filters if provided. */
|
||||
private static function applyTermFilters($q, ?string $semester, ?string $schoolYear)
|
||||
{
|
||||
if (!empty($semester)) {
|
||||
$query->where('semester', $semester);
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
if (!empty($schoolYear)) {
|
||||
$query->where('school_year', $schoolYear);
|
||||
if ($schoolYear !== null && $schoolYear !== '') {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
return $query;
|
||||
return $q;
|
||||
}
|
||||
|
||||
private function deriveTermFromDate(string $ymd): array
|
||||
/** Derive (semester, school_year) from a Y-m-d date (Fall=Aug–Dec; Spring=Jan–Jul). */
|
||||
private static function deriveTermFromDate(string $ymd): array
|
||||
{
|
||||
$d = Carbon::parse(substr($ymd, 0, 10));
|
||||
try {
|
||||
$d = Carbon::createFromFormat('Y-m-d', substr($ymd, 0, 10), 'UTC');
|
||||
} catch (\Throwable $e) {
|
||||
$d = Carbon::now('UTC');
|
||||
}
|
||||
|
||||
$month = $d->month;
|
||||
$year = $d->year;
|
||||
$m = (int) $d->format('n');
|
||||
$y = (int) $d->format('Y');
|
||||
|
||||
$semester = ($month >= 8 && $month <= 12) ? 'Fall' : 'Spring';
|
||||
$startY = ($month >= 8) ? $year : $year - 1;
|
||||
$semester = ($m >= 8 && $m <= 12) ? 'Fall' : 'Spring';
|
||||
$startY = ($m >= 8) ? $y : $y - 1;
|
||||
$endY = $startY + 1;
|
||||
|
||||
return [$semester, "{$startY}-{$endY}"];
|
||||
return [$semester, sprintf('%d-%d', $startY, $endY)];
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Main CRUD-ish API (converted from CI)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/** Day bounds for DATETIME searches (index-friendly). */
|
||||
private static function dayRange(string $ymd): array
|
||||
{
|
||||
$day = substr($ymd, 0, 10);
|
||||
$start = Carbon::parse($day, 'UTC')->startOfDay();
|
||||
$endEx = Carbon::parse($day, 'UTC')->addDay()->startOfDay(); // exclusive
|
||||
return [$start, $endEx];
|
||||
}
|
||||
|
||||
public function recordAbsence(
|
||||
/* ----------------------- CRUD-ish APIs ----------------------- */
|
||||
|
||||
/**
|
||||
* Records a student absence (stores time at 00:00:00 if only a day is given).
|
||||
* NOTE: $semester/$schoolYear are optional; will be derived from $date if omitted.
|
||||
* Returns inserted id.
|
||||
*/
|
||||
public static function recordAbsence(
|
||||
int $studentId,
|
||||
string $date,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null,
|
||||
bool $isExcused = false,
|
||||
?string $reason = null
|
||||
) {
|
||||
$dt = $this->normalizeDateTime($date);
|
||||
): int {
|
||||
$dt = static::normalizeDateTime($date);
|
||||
|
||||
if (!$semester || !$schoolYear) {
|
||||
[$derivedSemester, $derivedYear] = $this->deriveTermFromDate($dt);
|
||||
$semester = $semester ?? $derivedSemester;
|
||||
$schoolYear = $schoolYear ?? $derivedYear;
|
||||
if ($semester === null || $schoolYear === null) {
|
||||
[$sem, $yr] = static::deriveTermFromDate($dt);
|
||||
$semester = $semester ?? $sem;
|
||||
$schoolYear = $schoolYear ?? $yr;
|
||||
}
|
||||
|
||||
return self::create([
|
||||
'student_id' => $studentId,
|
||||
'date' => $dt,
|
||||
'is_reported' => $isExcused ? 1 : 0,
|
||||
'reason' => $reason,
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
])->id;
|
||||
$row = static::create([
|
||||
'student_id' => $studentId,
|
||||
'date' => $dt,
|
||||
'is_reported' => $isExcused ? 1 : 0,
|
||||
'reason' => $reason,
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
|
||||
return (int) $row->id;
|
||||
}
|
||||
|
||||
public function getStudentAbsences(
|
||||
/**
|
||||
* All absences for a student; start/end accept Y-m-d or Y-m-d H:i:s.
|
||||
* For day-only ranges, we use DATETIME bounds (>= startOfDay, < nextDay) for performance.
|
||||
*/
|
||||
public static function getStudentAbsences(
|
||||
int $studentId,
|
||||
?string $startDate = null,
|
||||
?string $endDate = null,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$query = self::where('student_id', $studentId);
|
||||
) {
|
||||
$q = static::query()->where('student_id', $studentId);
|
||||
|
||||
if ($startDate) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||
$query->whereDate('date', '>=', $startDate);
|
||||
[$start, ] = static::dayRange($startDate);
|
||||
$q->where('date', '>=', $start);
|
||||
} else {
|
||||
$query->where('date', '>=', $this->normalizeDateTime($startDate));
|
||||
$q->where('date', '>=', static::normalizeDateTime($startDate));
|
||||
}
|
||||
}
|
||||
|
||||
if ($endDate) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
|
||||
$query->whereDate('date', '<=', $endDate);
|
||||
[, $endEx] = static::dayRange($endDate);
|
||||
$q->where('date', '<', $endEx);
|
||||
} else {
|
||||
$query->where('date', '<=', $this->normalizeDateTime($endDate));
|
||||
$q->where('date', '<=', static::normalizeDateTime($endDate));
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
return $query->orderBy('date', 'DESC')->get()->toArray();
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
public function getUnnotifiedUnexcusedAbsences(
|
||||
/**
|
||||
* Unexcused + unnotified absences (optionally for a specific day).
|
||||
*/
|
||||
public static function getUnnotifiedUnexcusedAbsences(
|
||||
?string $date = null,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$query = self::where('is_reported', 0)->where('is_notified', 0);
|
||||
) {
|
||||
$q = static::query()
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
if ($date) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
$query->whereDate('date', $date);
|
||||
[$start, $endEx] = static::dayRange($date);
|
||||
$q->where('date', '>=', $start)->where('date', '<', $endEx);
|
||||
} else {
|
||||
$query->where('date', $this->normalizeDateTime($date));
|
||||
$q->where('date', static::normalizeDateTime($date));
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
return $query->orderBy('date', 'DESC')->get()->toArray();
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
public function hasConsecutiveAbsences(
|
||||
/**
|
||||
* Has at least N absences in the last N days (rough heuristic).
|
||||
*/
|
||||
public static function hasConsecutiveAbsences(
|
||||
int $studentId,
|
||||
int $days,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$start = Carbon::now()->subDays($days)->toDateString();
|
||||
$startDay = Carbon::now('UTC')->subDays($days)->toDateString();
|
||||
[$start, ] = static::dayRange($startDay);
|
||||
|
||||
$query = self::where('student_id', $studentId)
|
||||
->whereDate('date', '>=', $start);
|
||||
$q = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('date', '>=', $start);
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
return $query->count() >= $days;
|
||||
return $q->count() >= $days;
|
||||
}
|
||||
|
||||
public function checkConsecutiveAbsences(
|
||||
/**
|
||||
* TRUE if last $threshold absences are exactly weekly (7-day spacing).
|
||||
*/
|
||||
public static function checkConsecutiveAbsences(
|
||||
int $studentId,
|
||||
int $threshold,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$query = self::where('student_id', $studentId)
|
||||
$q = static::query()
|
||||
->select(['date', 'is_reported', 'is_notified'])
|
||||
->where('student_id', $studentId)
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
$absences = $query->orderBy('date', 'DESC')
|
||||
->limit($threshold)
|
||||
->get()
|
||||
->toArray();
|
||||
$absences = $q->orderByDesc('date')->limit($threshold)->get()->toArray();
|
||||
|
||||
if (count($absences) < $threshold) {
|
||||
return false;
|
||||
}
|
||||
if (count($absences) < $threshold) return false;
|
||||
|
||||
for ($i = 0; $i < $threshold - 1; $i++) {
|
||||
$d1 = Carbon::parse($absences[$i]['date']);
|
||||
$d2 = Carbon::parse($absences[$i + 1]['date']);
|
||||
if ($d2->diffInDays($d1) !== 7) {
|
||||
return false;
|
||||
}
|
||||
$d1 = new \DateTime($absences[$i]['date']);
|
||||
$d2 = new \DateTime($absences[$i + 1]['date']);
|
||||
$diffDays = (int) $d2->diff($d1)->format('%a');
|
||||
if ($diffDays !== 7) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkAbsencesInStreak(
|
||||
/**
|
||||
* TRUE if at least $absencesThreshold absences in the last $weeksStreak weeks.
|
||||
* (Mirrors your CI behavior: count of last N fetched records)
|
||||
*/
|
||||
public static function checkAbsencesInStreak(
|
||||
int $studentId,
|
||||
int $absencesThreshold,
|
||||
int $weeksStreak,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$query = self::where('student_id', $studentId)
|
||||
$q = static::query()
|
||||
->select(['date', 'is_reported', 'is_notified'])
|
||||
->where('student_id', $studentId)
|
||||
->where('is_reported', 0)
|
||||
->where('is_notified', 0);
|
||||
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
|
||||
$records = $query->orderBy('date', 'DESC')
|
||||
->limit($weeksStreak)
|
||||
->get()
|
||||
->toArray();
|
||||
$records = $q->orderByDesc('date')->limit($weeksStreak)->get()->toArray();
|
||||
|
||||
if (count($records) < $weeksStreak) {
|
||||
return false;
|
||||
if (count($records) < $weeksStreak) return false;
|
||||
|
||||
$count = 0;
|
||||
foreach ($records as $r) {
|
||||
if ((int)($r['is_reported'] ?? 0) === 0 && (int)($r['is_notified'] ?? 0) === 0) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
$count = collect($records)->filter(function ($r) {
|
||||
return $r['is_reported'] == 0 && $r['is_notified'] == 0;
|
||||
})->count();
|
||||
|
||||
return $count >= $absencesThreshold;
|
||||
}
|
||||
|
||||
public function getStudentAttendanceStats(
|
||||
/**
|
||||
* Summarize a student's status this term.
|
||||
*/
|
||||
public static function getStudentAttendanceStats(
|
||||
int $studentId,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): array {
|
||||
$base = self::where('student_id', $studentId);
|
||||
$this->applyTermFilters($base, $semester, $schoolYear);
|
||||
$q1 = static::query()->where('student_id', $studentId);
|
||||
static::applyTermFilters($q1, $semester, $schoolYear);
|
||||
$total = (int) $q1->count();
|
||||
|
||||
$totalAbs = (clone $base)->count();
|
||||
$unexcusedAbs = (clone $base)->where('is_reported', 0)->count();
|
||||
$last = (clone $base)->orderBy('date', 'DESC')->first();
|
||||
$q2 = static::query()->where('student_id', $studentId)->where('is_reported', 0);
|
||||
static::applyTermFilters($q2, $semester, $schoolYear);
|
||||
$unexcused = (int) $q2->count();
|
||||
|
||||
$q3 = static::query()->where('student_id', $studentId);
|
||||
static::applyTermFilters($q3, $semester, $schoolYear);
|
||||
$last = $q3->orderByDesc('date')->first();
|
||||
|
||||
return [
|
||||
'total_absences' => $totalAbs,
|
||||
'unexcused_absences' => $unexcusedAbs,
|
||||
'last_absence' => $last ? $last->toArray() : null,
|
||||
'total_absences' => $total,
|
||||
'unexcused_absences' => $unexcused,
|
||||
'last_absence' => $last ? $last->toArray() : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function getAttendanceBySemester(
|
||||
/**
|
||||
* Fetch records by semester/schoolYear (with optional limit).
|
||||
*/
|
||||
public static function getAttendanceBySemester(
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null,
|
||||
int $limit = 0
|
||||
): array {
|
||||
$query = self::query();
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
return $query->orderBy('date', 'DESC')->get()->toArray();
|
||||
) {
|
||||
$q = static::query();
|
||||
static::applyTermFilters($q, $semester, $schoolYear);
|
||||
if ($limit > 0) $q->limit($limit);
|
||||
return $q->orderByDesc('date')->get();
|
||||
}
|
||||
|
||||
public function getSemesterSummary(?string $semester = null, ?string $schoolYear = null): array
|
||||
/**
|
||||
* Summary counts for a term.
|
||||
*/
|
||||
public static function getSemesterSummary(?string $semester = null, ?string $schoolYear = null): array
|
||||
{
|
||||
$query = self::query();
|
||||
$this->applyTermFilters($query, $semester, $schoolYear);
|
||||
$q1 = static::query();
|
||||
static::applyTermFilters($q1, $semester, $schoolYear);
|
||||
$total = (int) $q1->count();
|
||||
|
||||
$total = (clone $query)->count();
|
||||
$unexcused = (clone $query)->where('is_reported', 0)->count();
|
||||
$students = (clone $query)->distinct()->count('student_id');
|
||||
$q2 = static::query()->where('is_reported', 0);
|
||||
static::applyTermFilters($q2, $semester, $schoolYear);
|
||||
$unexcused = (int) $q2->count();
|
||||
|
||||
$q3 = static::query();
|
||||
static::applyTermFilters($q3, $semester, $schoolYear);
|
||||
$students = (int) $q3->distinct('student_id')->count('student_id');
|
||||
|
||||
return [
|
||||
'total_absences' => $total,
|
||||
@@ -302,13 +343,11 @@ class AttendanceTracking extends BaseModel
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Violation Update System
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function updateAttendanceViolations(array $studentsWithViolations, string $semester, string $schoolYear): array
|
||||
/**
|
||||
* Update/create violation rows (matching by student + day-range(date) + term).
|
||||
* Expects input rows like: ['id'=>..., 'violation'=>..., 'absences'=>[['date'=>'Y-m-d'], ...]]
|
||||
*/
|
||||
public static function updateAttendanceViolations(array $studentsWithViolations, string $semester, string $schoolYear): array
|
||||
{
|
||||
$results = ['added' => 0, 'updated' => 0, 'skipped' => 0, 'notified' => 0];
|
||||
|
||||
@@ -319,46 +358,61 @@ class AttendanceTracking extends BaseModel
|
||||
}
|
||||
|
||||
foreach ($student['absences'] as $absence) {
|
||||
$ymd = substr($absence['date'] ?? now()->format('Y-m-d'), 0, 10);
|
||||
$dt = $this->normalizeDateTime($ymd);
|
||||
$ymd = substr((string)($absence['date'] ?? now()->toDateString()), 0, 10);
|
||||
[$start, $endEx] = static::dayRange($ymd);
|
||||
|
||||
$data = [
|
||||
'student_id' => (int)$student['id'],
|
||||
'date' => $dt,
|
||||
'is_reported' => 0,
|
||||
'reason' => $student['violation'] ?? '',
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'student_id' => (int)($student['id'] ?? 0),
|
||||
'date' => static::normalizeDateTime($ymd),
|
||||
'is_reported' => 0,
|
||||
'reason' => (string)($student['violation'] ?? ''),
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
];
|
||||
|
||||
$existing = self::where('student_id', $data['student_id'])
|
||||
->whereDate('date', $ymd)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
if ($data['student_id'] <= 0) {
|
||||
$results['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
if ($existing->reason !== $data['reason']) {
|
||||
$existing->update($data);
|
||||
$results['updated']++;
|
||||
try {
|
||||
// Match by student + day range + term
|
||||
$existing = static::query()
|
||||
->where('student_id', $data['student_id'])
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
if ($existing->is_notified) {
|
||||
$existing->update(['is_notified' => 0, 'notif_counter' => 0]);
|
||||
$results['notified']++;
|
||||
if ($existing) {
|
||||
if (($existing->reason ?? '') !== $data['reason']) {
|
||||
$existing->fill($data)->save();
|
||||
$results['updated']++;
|
||||
|
||||
if (!empty($existing->is_notified)) {
|
||||
static::resetNotification((int)$existing->id);
|
||||
$results['notified']++;
|
||||
}
|
||||
} else {
|
||||
$results['skipped']++;
|
||||
}
|
||||
} else {
|
||||
$results['skipped']++;
|
||||
}
|
||||
} else {
|
||||
$new = self::create($data);
|
||||
$results['added']++;
|
||||
$new = static::create($data);
|
||||
$results['added']++;
|
||||
|
||||
if (preg_match('/\b4\b/', $data['reason'])) {
|
||||
$new->update(['is_notified' => 1, 'notif_counter' => 1]);
|
||||
$results['notified']++;
|
||||
// Only match standalone "4"
|
||||
if (preg_match('/\b4\b/', $data['reason'])) {
|
||||
static::markAsNotified((int)$new->id);
|
||||
$results['notified']++;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// keep behavior: skip on error
|
||||
$results['skipped']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,102 +420,150 @@ class AttendanceTracking extends BaseModel
|
||||
return $results;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Notification operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/* ----------------------- Notification ops ----------------------- */
|
||||
|
||||
public function markAsNotified(int $id): bool
|
||||
public static function markAsNotified(int $id): bool
|
||||
{
|
||||
self::where('id', $id)->increment('notif_counter');
|
||||
return self::where('id', $id)->update(['is_notified' => 1]) > 0;
|
||||
// atomic increment + set flag
|
||||
$affected = static::query()
|
||||
->whereKey($id)
|
||||
->update([
|
||||
'notif_counter' => DB::raw('COALESCE(notif_counter,0)+1'),
|
||||
'is_notified' => 1,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
public function resetNotification(int $id): bool
|
||||
public static function resetNotification(int $id): bool
|
||||
{
|
||||
return self::where('id', $id)->update([
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0
|
||||
]) > 0;
|
||||
$affected = static::query()
|
||||
->whereKey($id)
|
||||
->update([
|
||||
'is_notified' => 0,
|
||||
'notif_counter' => 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected >= 0;
|
||||
}
|
||||
|
||||
public function markRuleAsNotified(
|
||||
/**
|
||||
* Mark rule as notified by matching latest row for that day + term.
|
||||
* $code is the rule code stored in "reason" (ABS_2, etc).
|
||||
* If no row exists, insert one.
|
||||
*/
|
||||
public static function markRuleAsNotified(
|
||||
int $studentId,
|
||||
string $code,
|
||||
string $ymd,
|
||||
?string $semester = null,
|
||||
?string $schoolYear = null
|
||||
): bool {
|
||||
$semester = $semester ?? config('school.semester');
|
||||
$schoolYear = $schoolYear ?? config('school.year');
|
||||
|
||||
$start = Carbon::parse($ymd)->startOfDay();
|
||||
$end = Carbon::parse($ymd)->addDay()->startOfDay();
|
||||
|
||||
$row = self::where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereBetween('date', [$start, $end])
|
||||
->where('reason', 'like', "%{$code}%")
|
||||
->orderBy('date', 'DESC')
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
$row = self::create([
|
||||
'student_id' => $studentId,
|
||||
'date' => $start,
|
||||
'is_reported' => 0,
|
||||
'reason' => $code,
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => 1,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
]);
|
||||
return true;
|
||||
// If you want to keep the old dependency on ConfigurationModel, you can inject it elsewhere.
|
||||
// Here we require term to be passed or derived from date.
|
||||
if ($semester === null || $schoolYear === null) {
|
||||
[$sem, $yr] = static::deriveTermFromDate($ymd);
|
||||
$semester = $semester ?? $sem;
|
||||
$schoolYear = $schoolYear ?? $yr;
|
||||
}
|
||||
|
||||
$count = ($row->notif_counter ?? 0) + 1;
|
||||
[$start, $endEx] = static::dayRange($ymd);
|
||||
|
||||
return $row->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $count,
|
||||
'is_reported' => 0,
|
||||
'updated_at' => now(),
|
||||
// 1) Match by code + day (preferred)
|
||||
$row = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->where('reason', 'like', "%{$code}%")
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
// 2) Fallback: match by day only
|
||||
if (!$row) {
|
||||
$row = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('date', '>=', $start)
|
||||
->where('date', '<', $endEx)
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($row) {
|
||||
$counter = ((int)($row->notif_counter ?? 0)) + 1;
|
||||
|
||||
$affected = static::query()
|
||||
->whereKey((int)$row->id)
|
||||
->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $counter,
|
||||
'is_reported' => 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
// Insert a row so future runs see it as notified
|
||||
$created = static::create([
|
||||
'student_id' => $studentId,
|
||||
'date' => static::normalizeDateTime($ymd),
|
||||
'is_reported' => 0,
|
||||
'reason' => $code,
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => 1,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'note' => null,
|
||||
]);
|
||||
|
||||
return !empty($created->id);
|
||||
}
|
||||
|
||||
public function markNotified(
|
||||
/**
|
||||
* Mark the latest matching tracking row as notified and increment counter.
|
||||
* Optionally bound by $lastDate (DATETIME string).
|
||||
*/
|
||||
public static function markNotified(
|
||||
int $studentId,
|
||||
string $schoolYear,
|
||||
?string $semester,
|
||||
string $code,
|
||||
?string $lastDate = null
|
||||
): int {
|
||||
$query = self::where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('reason', 'like', "%{$code}%");
|
||||
$q = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($semester) {
|
||||
$query->where('semester', $semester);
|
||||
if (!empty($semester)) {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
|
||||
if ($lastDate) {
|
||||
$query->where('date', '<=', $lastDate);
|
||||
if (!empty($lastDate)) {
|
||||
$q->where('date', '<=', static::normalizeDateTime($lastDate));
|
||||
}
|
||||
|
||||
$row = $query->orderBy('date', 'DESC')->first();
|
||||
$row = $q->where('reason', 'like', "%{$code}%")
|
||||
->orderByDesc('date')
|
||||
->first();
|
||||
|
||||
if (!$row) {
|
||||
return 0;
|
||||
}
|
||||
if (!$row) return 0;
|
||||
|
||||
$counter = ($row->notif_counter ?? 0) + 1;
|
||||
$counter = ((int)($row->notif_counter ?? 0)) + 1;
|
||||
|
||||
return $row->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $counter,
|
||||
'updated_at' => now(),
|
||||
]) ? 1 : 0;
|
||||
$affected = static::query()
|
||||
->whereKey((int)$row->id)
|
||||
->update([
|
||||
'is_notified' => 1,
|
||||
'notif_counter' => $counter,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $affected > 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class AuthorizedUser extends Model
|
||||
class AuthorizedUser extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'authorized_users'; // Table name
|
||||
protected $primaryKey = 'id';
|
||||
public $incrementing = true;
|
||||
protected $table = 'authorized_users';
|
||||
|
||||
// ✅ auto-manage created_at / updated_at
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'authorized_user_id',
|
||||
@@ -26,24 +26,22 @@ class AuthorizedUser extends Model
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
// Laravel manages created_at + updated_at automatically
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'authorized_user_id' => 'integer',
|
||||
'email' => 'string',
|
||||
'firstname' => 'string',
|
||||
'lastname' => 'string',
|
||||
'phone_number' => 'string',
|
||||
'relation_to_user' => 'string',
|
||||
'token' => 'string',
|
||||
'status' => 'string',
|
||||
];
|
||||
|
||||
// Relationship: Each authorized user belongs to a user
|
||||
/* =========================
|
||||
* Relationships (optional)
|
||||
* ========================= */
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
||||
public function authorizedUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'authorized_user_id');
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class AuthorizedUsers extends BaseModel
|
||||
{
|
||||
protected $table = 'authorized_users';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = true;
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'authorized_user_id',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'phone_number',
|
||||
'gender',
|
||||
'email',
|
||||
'relation_to_user',
|
||||
'token',
|
||||
'status',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class BadgePrintLog extends Model
|
||||
class BadgePrintLog extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'badge_print_logs';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = false; // matches CI model
|
||||
|
||||
// CI: useTimestamps = false
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'printed_by',
|
||||
@@ -23,27 +24,38 @@ class BadgePrintLog extends Model
|
||||
'copies',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'printed_by' => 'integer',
|
||||
'printed_at' => 'datetime',
|
||||
'copies' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Laravel version of CI4 logPrints()
|
||||
* Insert a batch of print logs. Swallows errors if table is missing.
|
||||
*
|
||||
* @param array $userIds
|
||||
* @param int|null $printedBy
|
||||
* @param int[] $userIds
|
||||
* @param int|null $printedBy
|
||||
* @param string|null $schoolYear
|
||||
* @param array $roleMap userId => role label
|
||||
* @param array $classMap userId => class name
|
||||
* @param int $copies
|
||||
* @return int rows inserted
|
||||
* @param array $roleMap userId => role label
|
||||
* @param array $classMap userId => class name
|
||||
* @param int $copies copies per user (defaults to 1)
|
||||
* @return int rows inserted (best-effort)
|
||||
*/
|
||||
public function logPrints(array $userIds, ?int $printedBy, ?string $schoolYear, array $roleMap = [], array $classMap = [], int $copies = 1): int
|
||||
{
|
||||
$now = Carbon::now('UTC')->toDateTimeString();
|
||||
public static function logPrints(
|
||||
array $userIds,
|
||||
?int $printedBy,
|
||||
?string $schoolYear,
|
||||
array $roleMap = [],
|
||||
array $classMap = [],
|
||||
int $copies = 1
|
||||
): int {
|
||||
$now = now();
|
||||
$rows = [];
|
||||
|
||||
foreach ($userIds as $uid) {
|
||||
$uid = (int)$uid;
|
||||
if ($uid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$uid = (int) $uid;
|
||||
if ($uid <= 0) continue;
|
||||
|
||||
$rows[] = [
|
||||
'user_id' => $uid,
|
||||
@@ -52,67 +64,78 @@ class BadgePrintLog extends Model
|
||||
'printed_at' => $now,
|
||||
'role' => (string)($roleMap[$uid] ?? ''),
|
||||
'class_section_name' => (string)($classMap[$uid] ?? ''),
|
||||
'copies' => max(1, (int)$copies),
|
||||
'copies' => max(1, (int) $copies),
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($rows)) {
|
||||
return 0;
|
||||
}
|
||||
if (empty($rows)) return 0;
|
||||
|
||||
try {
|
||||
DB::table($this->table)->insert($rows);
|
||||
// insert returns bool; we return count best-effort like CI
|
||||
DB::table((new static)->getTable())->insert($rows);
|
||||
return count($rows);
|
||||
} catch (QueryException $e) {
|
||||
// Avoid breaking badge/PDF generation if the table is missing or other DB error
|
||||
Log::error('BadgePrintLog::logPrints failed: ' . $e->getMessage());
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error("BadgePrintLog::logPrints failed: " . $e->getMessage());
|
||||
Log::error('BadgePrintLog::logPrints failed: ' . $e->getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel version of CI4 getStatus()
|
||||
* Return counts and last printed info for given user IDs.
|
||||
*
|
||||
* @param array $userIds
|
||||
* @param string|null $schoolYear
|
||||
* @return array
|
||||
* @param int[] $userIds
|
||||
* @param string|null $schoolYear Optional filter by school year
|
||||
* @return array userId => [count, last_printed_at, last_printed_by]
|
||||
*/
|
||||
public function getStatus(array $userIds, ?string $schoolYear = null): array
|
||||
public static function getStatus(array $userIds, ?string $schoolYear = null): array
|
||||
{
|
||||
$userIds = array_values(array_unique(array_map('intval', array_filter($userIds))));
|
||||
if (empty($userIds)) {
|
||||
return [];
|
||||
}
|
||||
if (empty($userIds)) return [];
|
||||
|
||||
try {
|
||||
$query = DB::table($this->table . ' as l')
|
||||
->selectRaw('
|
||||
l.user_id,
|
||||
COUNT(*) as prints,
|
||||
MAX(l.printed_at) as last_printed_at,
|
||||
MAX(l.printed_by) as last_printed_by
|
||||
')
|
||||
$q = DB::table((new static)->getTable() . ' as l')
|
||||
->selectRaw('l.user_id, COUNT(*) AS prints, MAX(l.printed_at) AS last_printed_at, MAX(l.printed_by) AS last_printed_by')
|
||||
->whereIn('l.user_id', $userIds)
|
||||
->groupBy('l.user_id');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$query->where('l.school_year', $schoolYear);
|
||||
$q->where('l.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$rows = $query->get();
|
||||
$rows = $q->get();
|
||||
} catch (QueryException $e) {
|
||||
Log::error('BadgePrintLog::getStatus failed: ' . $e->getMessage());
|
||||
return [];
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error("BadgePrintLog::getStatus failed: " . $e->getMessage());
|
||||
Log::error('BadgePrintLog::getStatus failed: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($rows as $row) {
|
||||
$result[(int)$row->user_id] = [
|
||||
'count' => (int)$row->prints,
|
||||
'last_printed_at' => (string)$row->last_printed_at,
|
||||
'last_printed_by' => $row->last_printed_by !== null ? (int)$row->last_printed_by : null,
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$uid = (int) ($r->user_id ?? 0);
|
||||
$out[$uid] = [
|
||||
'count' => (int) ($r->prints ?? 0),
|
||||
'last_printed_at' => (string) ($r->last_printed_at ?? ''),
|
||||
'last_printed_by' => isset($r->last_printed_by) ? (int) $r->last_printed_by : null,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional relationships */
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function printedByUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'printed_by');
|
||||
}
|
||||
}
|
||||
+142
-21
@@ -2,38 +2,159 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* BaseModel replaces the CodeIgniter Model and wires up the legacy builder
|
||||
* and database utility that the existing models expect.
|
||||
*/
|
||||
class BaseModel extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
private static ?array $tableColumnsCache = null;
|
||||
|
||||
public CiDatabase $db;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
public function getFillable(): array
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->db = CiDatabase::instance();
|
||||
if ($this->isTestingEnv()) {
|
||||
$columns = $this->tableColumns();
|
||||
if (!empty($columns)) {
|
||||
$key = $this->getKeyName();
|
||||
return array_values(array_filter($columns, static function ($col) use ($key) {
|
||||
return $col !== $key;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
$fillable = parent::getFillable();
|
||||
if ($this->timestamps) {
|
||||
if (!in_array('created_at', $fillable, true)) {
|
||||
$fillable[] = 'created_at';
|
||||
}
|
||||
if (!in_array('updated_at', $fillable, true)) {
|
||||
$fillable[] = 'updated_at';
|
||||
}
|
||||
}
|
||||
|
||||
return $fillable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure our legacy builder is used so the old helper methods remain available.
|
||||
*/
|
||||
public function newEloquentBuilder($query)
|
||||
private function isTestingEnv(): bool
|
||||
{
|
||||
return new CiModelBuilder($query);
|
||||
$env = getenv('APP_ENV')
|
||||
?: ($_SERVER['APP_ENV'] ?? ($_ENV['APP_ENV'] ?? null));
|
||||
|
||||
if ($env === 'testing') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (function_exists('app')) {
|
||||
try {
|
||||
$app = app();
|
||||
if ($app instanceof \Illuminate\Foundation\Application && $app->runningUnitTests()) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Fallback to env checks when the container isn't booted.
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the familiar CodeIgniter helper available on the model itself.
|
||||
*/
|
||||
public function findAll($limit = 0, $offset = 0)
|
||||
private function tableColumns(): array
|
||||
{
|
||||
return $this->newModelQuery()->findAll($limit, $offset);
|
||||
if (self::$tableColumnsCache === null) {
|
||||
self::$tableColumnsCache = $this->loadTableColumns();
|
||||
}
|
||||
|
||||
return self::$tableColumnsCache[$this->getTable()] ?? [];
|
||||
}
|
||||
|
||||
private function loadTableColumns(): array
|
||||
{
|
||||
$map = [];
|
||||
$pattern = null;
|
||||
if (function_exists('database_path')) {
|
||||
try {
|
||||
$pattern = database_path('migrations/*.php');
|
||||
} catch (\Throwable $e) {
|
||||
$pattern = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pattern === null) {
|
||||
$pattern = __DIR__ . '/../../database/migrations/*.php';
|
||||
}
|
||||
|
||||
$files = glob($pattern) ?: [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$contents = file_get_contents($file);
|
||||
if ($contents === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->parseSqlCreateTables($contents) as $table => $columns) {
|
||||
$map[$table] = $columns;
|
||||
}
|
||||
|
||||
foreach ($this->parseSchemaCreateTables($contents) as $table => $columns) {
|
||||
if (!isset($map[$table]) || empty($map[$table])) {
|
||||
$map[$table] = $columns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function parseSqlCreateTables(string $contents): array
|
||||
{
|
||||
$map = [];
|
||||
if (preg_match_all('/CREATE TABLE `([^`]+)`\\s*\\((.*?)\\)\\s*(?:ENGINE|;)/is', $contents, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$table = $match[1];
|
||||
$colsBlock = $match[2];
|
||||
preg_match_all('/`([^`]+)`\\s+[^,]+/m', $colsBlock, $colsMatch);
|
||||
$columns = $colsMatch[1] ?? [];
|
||||
if (!empty($columns)) {
|
||||
$map[$table] = $columns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function parseSchemaCreateTables(string $contents): array
|
||||
{
|
||||
$map = [];
|
||||
if (!preg_match_all('/Schema::create\\(\\s*[\'"]([^\'"]+)[\'"]\\s*,\\s*function\\s*\\([^)]*\\)\\s*\\{(.*?)\\n\\s*\\}\\);/is', $contents, $matches, PREG_SET_ORDER)) {
|
||||
return $map;
|
||||
}
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$table = $match[1];
|
||||
$body = $match[2];
|
||||
$columns = [];
|
||||
|
||||
if (preg_match_all('/\\$table->\\w+\\(\\s*[\'"]([^\'"]+)[\'"]/', $body, $colMatches)) {
|
||||
$columns = array_merge($columns, $colMatches[1]);
|
||||
}
|
||||
|
||||
if (str_contains($body, 'timestamps(') || str_contains($body, 'timestamps()')) {
|
||||
$columns[] = 'created_at';
|
||||
$columns[] = 'updated_at';
|
||||
}
|
||||
|
||||
if (str_contains($body, 'softDeletes(') || str_contains($body, 'softDeletes()')) {
|
||||
$columns[] = 'deleted_at';
|
||||
}
|
||||
|
||||
if (!empty($columns)) {
|
||||
$map[$table] = $columns;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class BroadcastEmail extends BaseModel
|
||||
{
|
||||
protected $table = 'broadcast_emails';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = true;
|
||||
protected $fillable = [
|
||||
'subject',
|
||||
'body',
|
||||
'total_sent',
|
||||
'created_at',
|
||||
];
|
||||
}
|
||||
+2
-32
@@ -2,36 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Calendar extends Model
|
||||
class Calendar extends CalendarEvent
|
||||
{
|
||||
protected $table = 'calendar_events';
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'date',
|
||||
'description',
|
||||
'notify_parent',
|
||||
'notify_admin',
|
||||
'notify_teacher',
|
||||
'no_school',
|
||||
'semester',
|
||||
'school_year',
|
||||
'notification_sent',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
/**
|
||||
* TODO: Manual port review required for custom CI query methods from Calendar.
|
||||
* - getEvents()
|
||||
* - getEventByDate()
|
||||
* - addEvent()
|
||||
* - getEventsBySchoolYear()
|
||||
* - getEventsBySchoolYearAndSemester()
|
||||
* - supportsEventType()
|
||||
*
|
||||
* This automated conversion preserves schema metadata only.
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CalendarEvent extends Model
|
||||
class CalendarEvent extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'calendar_events';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI uses created_at / updated_at
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'date',
|
||||
@@ -25,103 +28,129 @@ class CalendarEvent extends Model
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
// Equivalent to CI4 timestamps
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'notify_parent' => 'boolean',
|
||||
'notify_admin' => 'boolean',
|
||||
'notify_teacher'=> 'boolean',
|
||||
'no_school' => 'boolean',
|
||||
// If your column is DATE use 'date', if it's DATETIME use 'datetime'
|
||||
'date' => 'date',
|
||||
'notify_parent' => 'boolean',
|
||||
'notify_admin' => 'boolean',
|
||||
'notify_teacher' => 'boolean',
|
||||
'no_school' => 'integer',
|
||||
];
|
||||
|
||||
/* ----------------------------------------------------------------------
|
||||
* GETTERS – Same behavior as CodeIgniter
|
||||
* ---------------------------------------------------------------------- */
|
||||
private static ?bool $hasEventTypeColumn = null;
|
||||
|
||||
public function getEvents(): array
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
public static function getEvents()
|
||||
{
|
||||
$events = $this->orderBy('date', 'ASC')->get()->toArray();
|
||||
return $this->withNoSchoolFlag($events);
|
||||
$events = static::query()->get()->toArray();
|
||||
return static::withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
public function getEventByDate($date): array
|
||||
public static function getEventByDate(string $date): array
|
||||
{
|
||||
$events = $this->whereDate('date', $date)->get()->toArray();
|
||||
return $this->withNoSchoolFlag($events);
|
||||
$events = static::query()->where('date', $date)->get()->toArray();
|
||||
return static::withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
public function getEventsBySchoolYear(string $schoolYear): array
|
||||
public static function addEvent(array $data): int
|
||||
{
|
||||
$events = $this->where('school_year', $schoolYear)
|
||||
->orderBy('date', 'ASC')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
return $this->withNoSchoolFlag($events);
|
||||
$row = static::create($data);
|
||||
return (int) $row->id;
|
||||
}
|
||||
|
||||
public function getEventsBySchoolYearAndSemester(string $schoolYear, ?string $semester = null): array
|
||||
public static function getEventsBySchoolYear(string $schoolYear): array
|
||||
{
|
||||
$query = $this->where('school_year', $schoolYear);
|
||||
$events = static::query()
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('date', 'asc')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
if (!empty($semester)) {
|
||||
$query->where('semester', $semester);
|
||||
return static::withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
public static function getEventsBySchoolYearAndSemester(string $schoolYear, ?string $semester = null): array
|
||||
{
|
||||
$q = static::query()->where('school_year', $schoolYear);
|
||||
|
||||
if (is_string($semester) && $semester !== '') {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
|
||||
$events = $query->orderBy('date', 'ASC')->get()->toArray();
|
||||
$events = $q->orderBy('date', 'asc')->get()->toArray();
|
||||
|
||||
return $this->withNoSchoolFlag($events);
|
||||
return static::withNoSchoolFlag($events);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------
|
||||
* CREATE EVENT — Eloquent version of addEvent()
|
||||
* ---------------------------------------------------------------------- */
|
||||
public function addEvent(array $data)
|
||||
public static function supportsEventType(): bool
|
||||
{
|
||||
return $this->create($data);
|
||||
if (static::$hasEventTypeColumn === null) {
|
||||
static::$hasEventTypeColumn = static::columnExists((new static)->getTable(), 'event_type');
|
||||
}
|
||||
return (bool) static::$hasEventTypeColumn;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------
|
||||
* UTILITIES — SAME LOGIC YOU HAD IN CI4
|
||||
* ---------------------------------------------------------------------- */
|
||||
/* =========================
|
||||
* Helpers (ported from CI)
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Adds computed no_school flag if missing.
|
||||
* Add computed no_school flag to each event without requiring DB schema changes.
|
||||
*/
|
||||
protected function withNoSchoolFlag(array $events): array
|
||||
protected static function withNoSchoolFlag(array $events): array
|
||||
{
|
||||
return array_map(function ($event) {
|
||||
$event = (array) $event;
|
||||
|
||||
// Respect DB value if present; otherwise infer heuristically
|
||||
if (array_key_exists('no_school', $event)) {
|
||||
$event['no_school'] = (int)$event['no_school'];
|
||||
$event['no_school'] = (int) ($event['no_school'] ?? 0);
|
||||
} else {
|
||||
$event['no_school'] = $this->isNoSchoolEvent($event) ? 1 : 0;
|
||||
$event['no_school'] = static::isNoSchoolEvent($event) ? 1 : 0;
|
||||
}
|
||||
|
||||
return $event;
|
||||
|
||||
}, $events);
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic detection of no-school days from text.
|
||||
* Heuristic to determine "no school" days from event text.
|
||||
*/
|
||||
protected function isNoSchoolEvent(array $event): bool
|
||||
protected static function isNoSchoolEvent(array $event): bool
|
||||
{
|
||||
$title = strtolower($event['title'] ?? '');
|
||||
$desc = strtolower($event['description'] ?? '');
|
||||
$title = strtolower((string)($event['title'] ?? ''));
|
||||
$desc = strtolower((string)($event['description'] ?? ''));
|
||||
$text = $title . ' ' . $desc;
|
||||
|
||||
$keywords = ['no school', 'no-school', 'cancel', 'vacation', 'holiday', 'break'];
|
||||
|
||||
foreach ($keywords as $kw) {
|
||||
if ($kw !== '' && str_contains($text, $kw)) {
|
||||
if ($kw !== '' && strpos($text, $kw) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function columnExists(string $table, string $column): bool
|
||||
{
|
||||
$db = DB::connection();
|
||||
if ($db->getDriverName() === 'sqlite') {
|
||||
return Schema::hasColumn($table, $column);
|
||||
}
|
||||
|
||||
$database = $db->getDatabaseName();
|
||||
|
||||
$count = $db->table('information_schema.columns')
|
||||
->where('table_schema', $database)
|
||||
->where('table_name', $table)
|
||||
->where('column_name', $column)
|
||||
->count();
|
||||
|
||||
return $count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class CiQueryResult extends Collection
|
||||
|
||||
@@ -2,16 +2,6 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class ClassModel extends BaseModel
|
||||
class ClassModel extends SchoolClass
|
||||
{
|
||||
protected $table = 'classes';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = false;
|
||||
protected $fillable = [
|
||||
'class_name',
|
||||
'schedule',
|
||||
'capacity',
|
||||
'semester',
|
||||
'school_year',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ClassPrepAdjustment extends Model
|
||||
class ClassPrepAdjustment extends BaseModel
|
||||
{
|
||||
protected $table = 'class_prep_adjustments';
|
||||
|
||||
// CI model didn't use timestamps; keep off unless your table has updated_at/created_at auto-managed.
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'class_section_id',
|
||||
'item_name',
|
||||
@@ -15,5 +19,15 @@ class ClassPrepAdjustment extends Model
|
||||
'school_year',
|
||||
'created_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'class_section_id' => 'integer',
|
||||
'adjustment' => 'integer', // change to 'decimal:2' if it's not an int
|
||||
];
|
||||
|
||||
// Optional relationship
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class ClassPreparation extends BaseModel
|
||||
{
|
||||
protected $table = 'class_preparation';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = true;
|
||||
protected $fillable = [
|
||||
'class_id',
|
||||
'school_year',
|
||||
'requirements',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ClassPreparationLog extends Model
|
||||
class ClassPreparationLog extends BaseModel
|
||||
{
|
||||
protected $table = 'class_preparation_log';
|
||||
|
||||
// CI: useTimestamps = false
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'class_section_id',
|
||||
'class_section',
|
||||
@@ -14,5 +18,16 @@ class ClassPreparationLog extends Model
|
||||
'prep_data',
|
||||
'created_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'class_section_id' => 'integer',
|
||||
// If prep_data is JSON in DB, use 'array' (or 'json') cast:
|
||||
'prep_data' => 'array',
|
||||
];
|
||||
|
||||
// Optional relationship
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ClassProgressAttachment extends Model
|
||||
class ClassProgressAttachment extends BaseModel
|
||||
{
|
||||
protected $table = 'class_progress_attachments';
|
||||
|
||||
// CI: useTimestamps = false
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'report_id',
|
||||
'file_path',
|
||||
@@ -15,5 +19,16 @@ class ClassProgressAttachment extends Model
|
||||
'file_size',
|
||||
'created_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'report_id' => 'integer',
|
||||
'file_size' => 'integer',
|
||||
];
|
||||
|
||||
// Optional relationship
|
||||
public function report()
|
||||
{
|
||||
// replace ClassProgressReport::class with your actual report model if different
|
||||
return $this->belongsTo(ClassProgressReport::class, 'report_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ClassProgressReport extends Model
|
||||
class ClassProgressReport extends BaseModel
|
||||
{
|
||||
protected $table = 'class_progress_reports';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'class_section_id',
|
||||
'teacher_id',
|
||||
@@ -28,5 +32,32 @@ class ClassProgressReport extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'class_section_id' => 'integer',
|
||||
'teacher_id' => 'integer',
|
||||
|
||||
// change to 'datetime' if your columns are DATETIME
|
||||
'week_start' => 'date',
|
||||
'week_end' => 'date',
|
||||
|
||||
// If flags_json is JSON in DB, this makes it an array automatically
|
||||
'flags_json' => 'array',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
public function teacher()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'teacher_id');
|
||||
}
|
||||
|
||||
public function attachments()
|
||||
{
|
||||
return $this->hasMany(ClassProgressAttachment::class, 'report_id');
|
||||
}
|
||||
}
|
||||
+82
-73
@@ -1,12 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ClassSection extends BaseModel
|
||||
{
|
||||
protected $table = 'classSection'; // Correct table name
|
||||
protected $primaryKey = 'id'; // Specify the primary key field
|
||||
// CI table name is "classSection"
|
||||
protected $table = 'classSection';
|
||||
|
||||
// ✅ CI: useTimestamps = true
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'class_id',
|
||||
'class_section_id',
|
||||
@@ -16,115 +22,118 @@ class ClassSection extends BaseModel
|
||||
'semester',
|
||||
'school_year',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
// Set default return type
|
||||
protected $casts = [
|
||||
'class_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
];
|
||||
|
||||
// Method to get class sections with class and section names
|
||||
public function getClassSections(?string $schoolYear = null, ?string $semester = null)
|
||||
/* =========================
|
||||
* Relationships (optional)
|
||||
* ========================= */
|
||||
public function schoolClass()
|
||||
{
|
||||
$builder = $this->select('ClassSection.*', 'classes.class_name', 'sections.section_name')
|
||||
->join('classes', 'ClassSection.class_id', '=', 'classes.id')
|
||||
->leftJoin('sections', 'ClassSection.class_section_id', '=', 'sections.id');
|
||||
|
||||
if ($schoolYear !== null) {
|
||||
$builder->where('ClassSection.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester !== null) {
|
||||
$builder->where('ClassSection.semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
// If you used SchoolClass for classes model name
|
||||
return $this->belongsTo(SchoolClass::class, 'class_id');
|
||||
}
|
||||
|
||||
public function getCurrentAcademicSection(int $classId, string $schoolYear, string $semester): ?array
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Get class sections with class name (like CI join result).
|
||||
* Returns array of rows: class_section_id, class_section_name, class_name
|
||||
*/
|
||||
public static function getClassSections(): array
|
||||
{
|
||||
return $this->newQuery()
|
||||
->where('class_id', $classId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->first();
|
||||
return DB::table('classSection')
|
||||
->leftJoin('classes', 'classSection.class_id', '=', 'classes.id')
|
||||
->select('classSection.class_section_id', 'classSection.class_section_name', 'classes.class_name')
|
||||
->orderBy('classSection.class_section_name', 'ASC')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent class_id for a given class_section_id.
|
||||
*
|
||||
* @param int|string $classSectionId
|
||||
* @return int|null class_id if found, otherwise null
|
||||
*/
|
||||
public function getClassId($classSectionId): ?int
|
||||
{
|
||||
if ($classSectionId === null || $classSectionId === '') {
|
||||
return null;
|
||||
* Get the parent class_id for a given class_section_id.
|
||||
*/
|
||||
public static function getClassId($classSectionId): ?int
|
||||
{
|
||||
if ($classSectionId === null || $classSectionId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = static::query()
|
||||
->select('class_id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->orderByDesc('id') // in case of duplicates, take latest
|
||||
->first();
|
||||
|
||||
return $row ? (int) $row->class_id : null;
|
||||
}
|
||||
|
||||
$row = $this->select('class_id')
|
||||
->where('class_section_id', $classSectionId)
|
||||
->orderBy($this->primaryKey, 'DESC') // in case of duplicates, take latest
|
||||
->first();
|
||||
|
||||
return $row ? (int) $row['class_id'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class_section_name for a given section_id.
|
||||
*
|
||||
* @param int|string $sectionId
|
||||
* @return string|null
|
||||
*/
|
||||
public function getClassSectionNameBySectionId($sectionId)
|
||||
public static function getClassSectionNameBySectionId($sectionId): ?string
|
||||
{
|
||||
$result = $this->where('class_section_id', $sectionId)->first();
|
||||
return $result['class_section_name'] ?? null;
|
||||
$row = static::query()
|
||||
->select('class_section_name')
|
||||
->where('class_section_id', $sectionId)
|
||||
->first();
|
||||
|
||||
return $row?->class_section_name;
|
||||
}
|
||||
|
||||
public function getClassSectionNameByClassId($classId)
|
||||
public static function getClassSectionNameByClassId($classId): ?string
|
||||
{
|
||||
$result = $this->where('class_id', $classId)->first();
|
||||
return $result['class_section_name'] ?? null;
|
||||
$row = static::query()
|
||||
->select('class_section_name')
|
||||
->where('class_id', $classId)
|
||||
->first();
|
||||
|
||||
return $row?->class_section_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get section ID from class section name.
|
||||
*
|
||||
* @param string $classSectionName The name of the class section.
|
||||
* @return mixed The section ID or null if no match is found.
|
||||
*/
|
||||
public function getSectionIDFromClassSectionName($classSectionName)
|
||||
public static function getSectionIDFromClassSectionName(string $classSectionName): ?int
|
||||
{
|
||||
// Query the table to find the class section by its name
|
||||
$result = $this->where('class_section_name', $classSectionName)
|
||||
->first(); // Get the first result (assuming class section name is unique)
|
||||
$row = static::query()
|
||||
->select('class_section_id')
|
||||
->where('class_section_name', $classSectionName)
|
||||
->first();
|
||||
|
||||
// Return the class section_id if the result is found, otherwise return null
|
||||
return $result ? $result['class_section_id'] : null;
|
||||
return $row ? (int) $row->class_section_id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base (non-letter) section row for a given class_id. E.g., '3' vs '3-A'.
|
||||
*/
|
||||
public function getBaseSectionByClassId(int $classId): ?array
|
||||
public static function getBaseSectionByClassId(int $classId): ?array
|
||||
{
|
||||
// Heuristic: names without a dash are base sections (e.g., '3', 'KG', 'youth')
|
||||
return $this->where('class_id', $classId)
|
||||
->where("class_section_name NOT LIKE '%-%'", null, false)
|
||||
$row = static::query()
|
||||
->where('class_id', $classId)
|
||||
->whereRaw("class_section_name NOT LIKE '%-%'")
|
||||
->orderBy('id', 'ASC')
|
||||
->first();
|
||||
|
||||
return $row ? $row->toArray() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lettered sections for a class (e.g., '3-A','3-B',...). Ordered by name asc.
|
||||
*/
|
||||
public function getLetterSectionsByClassId(int $classId): array
|
||||
public static function getLetterSectionsByClassId(int $classId): array
|
||||
{
|
||||
return $this->where('class_id', $classId)
|
||||
->like('class_section_name', '-', 'both')
|
||||
return static::query()
|
||||
->where('class_id', $classId)
|
||||
->where('class_section_name', 'like', '%-%')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->findAll();
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Classroom extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'classes';
|
||||
protected $primaryKey = 'id';
|
||||
protected $fillable = [
|
||||
'class_name',
|
||||
'schedule',
|
||||
'capacity',
|
||||
'semester',
|
||||
'school_year',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
/* -----------------------------------------------
|
||||
* Equivalent CI4 functions rewritten for Laravel
|
||||
* ----------------------------------------------- */
|
||||
|
||||
public function getAllClasses()
|
||||
{
|
||||
return self::all();
|
||||
}
|
||||
|
||||
public function getClassById($id)
|
||||
{
|
||||
return self::find($id);
|
||||
}
|
||||
|
||||
public function createClass(array $data)
|
||||
{
|
||||
return self::create($data); // returns the model instance
|
||||
}
|
||||
|
||||
public function updateClass($id, array $data)
|
||||
{
|
||||
$class = self::find($id);
|
||||
if (!$class) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $class->update($data);
|
||||
}
|
||||
|
||||
public function deleteClass($id)
|
||||
{
|
||||
return self::destroy($id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class Communication extends CommunicationLog
|
||||
{
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class CommunicationLog extends Model
|
||||
class CommunicationLog extends BaseModel
|
||||
{
|
||||
protected $table = 'communication_logs';
|
||||
|
||||
// CI model didn't enable timestamps; keep off unless your table has created_at/updated_at.
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'family_id',
|
||||
@@ -23,5 +27,28 @@ class CommunicationLog extends Model
|
||||
'sent_by',
|
||||
'metadata',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'family_id' => 'integer',
|
||||
'sent_by' => 'integer',
|
||||
|
||||
// If these columns are JSON in DB, this will auto convert to arrays
|
||||
'recipients' => 'array',
|
||||
'cc' => 'array',
|
||||
'bcc' => 'array',
|
||||
'attachments' => 'array',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function sender()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'sent_by');
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,19 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Competition extends Model
|
||||
class Competition extends BaseModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $table = 'competitions';
|
||||
|
||||
// ✅ CI: useSoftDeletes = true
|
||||
use SoftDeletes;
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at) + deleted_at for soft deletes
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'semester',
|
||||
@@ -28,5 +34,43 @@ class Competition extends Model
|
||||
'locked_at',
|
||||
'locked_by',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'class_section_id' => 'integer',
|
||||
'winners_count' => 'integer',
|
||||
'prize_per_point' => 'decimal:2', // adjust precision if needed
|
||||
|
||||
'is_published' => 'boolean',
|
||||
'is_locked' => 'boolean',
|
||||
|
||||
// change to 'date' if your columns are DATE (not DATETIME)
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'published_at' => 'datetime',
|
||||
'locked_at' => 'datetime',
|
||||
|
||||
'locked_by' => 'integer',
|
||||
'created_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
public function lockedByUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'locked_by');
|
||||
}
|
||||
|
||||
public function createdByUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function classWinners()
|
||||
{
|
||||
return $this->hasMany(CompetitionClassWinner::class, 'competition_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class CompetitionClassWinner extends Model
|
||||
class CompetitionClassWinner extends BaseModel
|
||||
{
|
||||
protected $table = 'competition_class_winners';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'competition_id',
|
||||
'class_section_id',
|
||||
@@ -21,5 +25,23 @@ class CompetitionClassWinner extends Model
|
||||
'prize_5',
|
||||
'prize_6',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'competition_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'question_count' => 'integer',
|
||||
// If winners_override is JSON in DB, cast to array:
|
||||
'winners_override' => 'array',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function competition()
|
||||
{
|
||||
return $this->belongsTo(Competition::class, 'competition_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class CompetitionScore extends Model
|
||||
class CompetitionScore extends BaseModel
|
||||
{
|
||||
protected $table = 'competition_scores';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'competition_id',
|
||||
'student_id',
|
||||
@@ -16,5 +20,27 @@ class CompetitionScore extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'competition_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'score' => 'integer', // change to 'decimal:2' if score is not int
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function competition()
|
||||
{
|
||||
return $this->belongsTo(Competition::class, 'competition_id');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class CompetitionWinner extends Model
|
||||
class CompetitionWinner extends BaseModel
|
||||
{
|
||||
protected $table = 'competition_winners';
|
||||
|
||||
// CI: useTimestamps = false
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'competition_id',
|
||||
'student_id',
|
||||
@@ -16,5 +20,30 @@ class CompetitionWinner extends Model
|
||||
'prize_amount',
|
||||
'created_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'competition_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'rank' => 'integer',
|
||||
'score' => 'integer', // change to 'decimal:2' if needed
|
||||
'prize_amount' => 'decimal:2', // adjust precision if needed
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function competition()
|
||||
{
|
||||
return $this->belongsTo(Competition::class, 'competition_id');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
@@ -3,78 +3,116 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Configuration extends BaseModel
|
||||
{
|
||||
protected $table = 'configuration'; // The name of the table
|
||||
protected $primaryKey = 'id'; // The primary key of the table
|
||||
protected $table = 'configuration';
|
||||
|
||||
// CI model didn't use timestamps
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'config_key',
|
||||
'config_value',
|
||||
];
|
||||
|
||||
public $timestamps = false;
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Get configuration value by key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string|null
|
||||
* Get configuration value by key (deterministic in case duplicates exist).
|
||||
*/
|
||||
public function getConfigValueByKey(string $key)
|
||||
public static function getConfigValueByKey(string $key): ?string
|
||||
{
|
||||
// Deterministic read in case historical duplicates exist
|
||||
$result = $this->where('config_key', $key)
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
return $result ? $result['config_value'] : null;
|
||||
try {
|
||||
$row = static::query()
|
||||
->where('config_key', $key)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
} catch (\Throwable $e) {
|
||||
if (app()->runningInConsole()) {
|
||||
return null;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $row ? (string) $row->config_value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set configuration value by key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return bool
|
||||
* If duplicates exist, update ALL of them to avoid inconsistent reads.
|
||||
*/
|
||||
public function setConfigValueByKey(string $key, string $value): bool
|
||||
public static function setConfigValueByKey(string $key, string $value): bool
|
||||
{
|
||||
// If one or more rows exist for this key, update ALL of them to avoid
|
||||
// inconsistent reads when duplicates are present.
|
||||
$count = $this->where('config_key', $key)->countAllResults();
|
||||
$count = static::query()->where('config_key', $key)->count();
|
||||
|
||||
if ($count > 0) {
|
||||
// Use a direct builder update scoped by key to affect all matches
|
||||
$ok = (bool) $this->db->table($this->table)
|
||||
$affected = static::query()
|
||||
->where('config_key', $key)
|
||||
->update(['config_value' => $value]);
|
||||
return $ok;
|
||||
|
||||
return $affected >= 0;
|
||||
}
|
||||
|
||||
// Insert a new record if none exist
|
||||
return $this->insert(['config_key' => $key, 'config_value' => $value]) !== false;
|
||||
$row = static::query()->create([
|
||||
'config_key' => $key,
|
||||
'config_value' => $value,
|
||||
]);
|
||||
|
||||
return !empty($row->id);
|
||||
}
|
||||
|
||||
// Method to retrieve all configuration data
|
||||
public function getAllConfigs()
|
||||
/**
|
||||
* Retrieve all configuration rows.
|
||||
*/
|
||||
public static function getAllConfigs()
|
||||
{
|
||||
return $this->findAll();
|
||||
return static::query()->get();
|
||||
}
|
||||
|
||||
// Method to update configuration by key
|
||||
public function updateConfig($id, $data)
|
||||
/**
|
||||
* Update configuration by id.
|
||||
*/
|
||||
public static function updateConfig(int $id, array $data): bool
|
||||
{
|
||||
return $this->update($id, $data);
|
||||
return static::query()->whereKey($id)->update($data) >= 0;
|
||||
}
|
||||
|
||||
// Method to add new configuration
|
||||
public function addConfig($data)
|
||||
/**
|
||||
* Add new configuration row.
|
||||
*/
|
||||
public static function addConfig(array $data): int
|
||||
{
|
||||
return $this->insert($data);
|
||||
$row = static::query()->create($data);
|
||||
return (int) $row->id;
|
||||
}
|
||||
|
||||
public function getConfig($key)
|
||||
/**
|
||||
* Backward-compatible helper (keeps your "semester" special-case).
|
||||
*
|
||||
* NOTE: In Laravel it’s better to call SemesterRangeService directly from services/controllers,
|
||||
* but this keeps behavior consistent with the CI model.
|
||||
*/
|
||||
public static function getConfig(string $key): ?string
|
||||
{
|
||||
return $this->getConfigValueByKey((string) $key);
|
||||
$key = (string) $key;
|
||||
|
||||
if ($key === 'semester') {
|
||||
try {
|
||||
// If you have this service in Laravel, inject/resolve it from container.
|
||||
// This call assumes the service constructor can accept Configuration model (like CI did).
|
||||
$semester = app(\App\Services\SemesterRangeService::class)->getSemesterForDate();
|
||||
if (is_string($semester) && $semester !== '') {
|
||||
return $semester;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore and fall back
|
||||
}
|
||||
}
|
||||
|
||||
return static::getConfigValueByKey($key);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-44
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
@@ -8,9 +7,10 @@ use App\Models\BaseModel;
|
||||
class ContactUs extends BaseModel
|
||||
{
|
||||
protected $table = 'contactus';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'sender_id',
|
||||
'reciever_id',
|
||||
@@ -22,71 +22,61 @@ class ContactUs extends BaseModel
|
||||
'school_year',
|
||||
];
|
||||
|
||||
// Timestamps (set manually or use auto timestamps below)
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
'sender_id' => 'required|integer',
|
||||
'reciever_id' => 'required|integer',
|
||||
'subject' => 'required|string|max_length[255]',
|
||||
'message' => 'required|string',
|
||||
'semester' => 'required|string|max_length[255]',
|
||||
'school_year' => 'permit_empty|string|max_length[9]'
|
||||
protected $casts = [
|
||||
'sender_id' => 'integer',
|
||||
'reciever_id' => 'integer',
|
||||
];
|
||||
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
/* Optional relationships */
|
||||
public function sender()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'sender_id');
|
||||
}
|
||||
|
||||
public function receiver()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reciever_id');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Retrieve messages by semester and school year.
|
||||
*
|
||||
* @param string $semester
|
||||
* @param string $school_year
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesBySemesterAndYear($semester, $school_year)
|
||||
public static function getMessagesBySemesterAndYear(string $semester, string $schoolYear)
|
||||
{
|
||||
return $this->where([
|
||||
'semester' => $semester,
|
||||
'school_year' => $school_year
|
||||
])->findAll();
|
||||
return static::query()
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve messages for a specific sender or receiver.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesForUser($userId)
|
||||
public static function getMessagesForUser(int $userId)
|
||||
{
|
||||
return $this->where('sender_id', $userId)
|
||||
return static::query()
|
||||
->where('sender_id', $userId)
|
||||
->orWhere('reciever_id', $userId)
|
||||
->findAll();
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a message by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function getMessageById($id)
|
||||
public static function getMessageById(int $id): ?self
|
||||
{
|
||||
return $this->find($id);
|
||||
return static::query()->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a message by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function updateMessage($id, $data)
|
||||
public static function updateMessage(int $id, array $data): bool
|
||||
{
|
||||
return $this->update($id, $data);
|
||||
return static::query()->whereKey($id)->update($data) >= 0;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class CurrentFlag extends Model
|
||||
class CurrentFlag extends BaseModel
|
||||
{
|
||||
protected $table = 'current_flag';
|
||||
|
||||
// CI model didn't specify timestamps. Since the table has created_at/updated_at columns,
|
||||
// you can enable auto-management. If your table does NOT actually have them, set false.
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'student_name',
|
||||
@@ -26,5 +31,33 @@ class CurrentFlag extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'updated_by_open' => 'integer',
|
||||
'updated_by_closed' => 'integer',
|
||||
'updated_by_canceled'=> 'integer',
|
||||
'flag_datetime' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function openedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by_open');
|
||||
}
|
||||
|
||||
public function closedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by_closed');
|
||||
}
|
||||
|
||||
public function canceledBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by_canceled');
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DiscountUsage extends BaseModel
|
||||
{
|
||||
protected $table = 'discount_usages';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: timestamps enabled (created_at / updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'voucher_id',
|
||||
'invoice_id',
|
||||
@@ -21,43 +25,38 @@ class DiscountUsage extends BaseModel
|
||||
'updated_at',
|
||||
'semester',
|
||||
];
|
||||
|
||||
// Enable automatic timestamps if desired (optional: only if you add created_at/updated_at columns)
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
// Add validation rules
|
||||
protected $validationRules = [
|
||||
'voucher_id' => 'required|is_natural_no_zero',
|
||||
'invoice_id' => 'required|is_natural_no_zero',
|
||||
'user_id' => 'permit_empty|is_natural_no_zero',
|
||||
'used_at' => 'valid_date'
|
||||
protected $casts = [
|
||||
'voucher_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'discount_amount' => 'decimal:2', // adjust precision if needed
|
||||
'updated_by' => 'integer',
|
||||
'parent_id' => 'integer',
|
||||
'used_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'voucher_id' => [
|
||||
'required' => 'Voucher ID is required.'
|
||||
],
|
||||
'invoice_id' => [
|
||||
'required' => 'Invoice ID is required.'
|
||||
]
|
||||
];
|
||||
|
||||
public function getTotalDiscountByParentIdAndSchoolYear(int $parentId, string $schoolYear): float
|
||||
/* Optional relationships */
|
||||
public function invoice()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$result = $db->table('discount_usages du')
|
||||
->selectSum('du.discount_amount', 'total_discount')
|
||||
->join('invoices i', 'du.invoice_id = i.id')
|
||||
->where('i.parent_id', $parentId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return $result && isset($result['total_discount']) ? (float) $result['total_discount'] : 0.00;
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
}
|
||||
public function voucher()
|
||||
{
|
||||
return $this->belongsTo(Voucher::class, 'voucher_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getTotalDiscountByParentIdAndSchoolYear()
|
||||
*/
|
||||
public static function getTotalDiscountByParentIdAndSchoolYear(int $parentId, string $schoolYear): float
|
||||
{
|
||||
$total = DB::table('discount_usages as du')
|
||||
->join('invoices as i', 'du.invoice_id', '=', 'i.id')
|
||||
->where('i.parent_id', $parentId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->sum('du.discount_amount');
|
||||
|
||||
return (float) $total;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DiscountVoucher extends BaseModel
|
||||
{
|
||||
protected $table = 'discount_vouchers';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI manages created_at / updated_at
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'discount_type',
|
||||
@@ -23,112 +29,123 @@ class DiscountVoucher extends BaseModel
|
||||
'semester',
|
||||
];
|
||||
|
||||
// Timestamps managed by CI
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
// Basic validation
|
||||
protected $validationRules = [
|
||||
'code' => 'required|alpha_numeric_punct|max_length[50]|is_unique[discount_vouchers.code,id,{id}]',
|
||||
'discount_type' => 'required|in_list[percent,fixed]',
|
||||
'discount_value' => 'required|numeric|greater_than_equal_to[0]',
|
||||
'max_uses' => 'permit_empty|is_natural',
|
||||
'valid_from' => 'permit_empty|valid_date[Y-m-d]',
|
||||
'valid_until' => 'permit_empty|valid_date[Y-m-d]',
|
||||
'is_active' => 'in_list[0,1]',
|
||||
'description' => 'permit_empty|string|max_length[1000]', // <-- NEW
|
||||
protected $casts = [
|
||||
'discount_value' => 'decimal:2',
|
||||
'max_uses' => 'integer',
|
||||
'times_used' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'valid_from' => 'date',
|
||||
'valid_until' => 'date',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'code' => [
|
||||
'is_unique' => 'The voucher code must be unique.',
|
||||
],
|
||||
'discount_type' => [
|
||||
'in_list' => 'The discount type must be either "percent" or "fixed".',
|
||||
],
|
||||
];
|
||||
/* =========================
|
||||
* Model hooks (CI beforeInsert/beforeUpdate equivalents)
|
||||
* ========================= */
|
||||
|
||||
// Normalize inputs before save
|
||||
protected $beforeInsert = ['normalizeFields', 'validateDatesOrder'];
|
||||
protected $beforeUpdate = ['normalizeFields', 'validateDatesOrder'];
|
||||
|
||||
protected function normalizeFields(array $data): array
|
||||
protected static function booted(): void
|
||||
{
|
||||
if (!isset($data['data'])) return $data;
|
||||
static::creating(function (self $m) {
|
||||
$m->normalizeFields();
|
||||
$m->validateDatesOrder(); // throws if invalid
|
||||
});
|
||||
|
||||
static::updating(function (self $m) {
|
||||
$m->normalizeFields();
|
||||
$m->validateDatesOrder(); // throws if invalid
|
||||
});
|
||||
}
|
||||
|
||||
private function normalizeFields(): void
|
||||
{
|
||||
// Normalize code: keep A-Z, 0-9 and dashes, uppercased
|
||||
if (array_key_exists('code', $data['data'])) {
|
||||
$raw = (string) $data['data']['code'];
|
||||
$normalized = strtoupper(preg_replace('/[^A-Z0-9\-]/i', '', trim($raw)));
|
||||
$data['data']['code'] = $normalized;
|
||||
if ($this->isDirty('code') && $this->code !== null) {
|
||||
$raw = (string) $this->code;
|
||||
$this->code = strtoupper(preg_replace('/[^A-Z0-9\-]/i', '', trim($raw)));
|
||||
}
|
||||
|
||||
// Empty strings -> NULL for dates & max_uses
|
||||
foreach (['valid_from', 'valid_until'] as $f) {
|
||||
if (array_key_exists($f, $data['data'])) {
|
||||
$v = trim((string) $data['data'][$f]);
|
||||
$data['data'][$f] = ($v === '') ? null : $v;
|
||||
if ($this->isDirty($f)) {
|
||||
$v = $this->{$f};
|
||||
if (is_string($v)) {
|
||||
$v = trim($v);
|
||||
$this->{$f} = ($v === '') ? null : $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (array_key_exists('max_uses', $data['data'])) {
|
||||
$v = $data['data']['max_uses'];
|
||||
$data['data']['max_uses'] = ($v === '' || $v === null) ? null : max(0, (int) $v);
|
||||
|
||||
if ($this->isDirty('max_uses')) {
|
||||
$v = $this->max_uses;
|
||||
if ($v === '' || $v === null) {
|
||||
$this->max_uses = null;
|
||||
} else {
|
||||
$this->max_uses = max(0, (int) $v);
|
||||
}
|
||||
}
|
||||
|
||||
// Coerce booleans
|
||||
if (array_key_exists('is_active', $data['data'])) {
|
||||
$data['data']['is_active'] = (int) !!$data['data']['is_active'];
|
||||
if ($this->isDirty('is_active')) {
|
||||
$this->is_active = (bool) $this->is_active;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function validateDatesOrder(array $data): array
|
||||
private function validateDatesOrder(): void
|
||||
{
|
||||
if (!isset($data['data'])) return $data;
|
||||
|
||||
$from = $data['data']['valid_from'] ?? null;
|
||||
$until = $data['data']['valid_until'] ?? null;
|
||||
|
||||
if ($from && $until && $from > $until) {
|
||||
// Inject a model error; save() will fail and return errors()
|
||||
$this->validator->setError('valid_until', 'Valid Until must be the same as or after Valid From.');
|
||||
if (!empty($this->valid_from) && !empty($this->valid_until)) {
|
||||
$from = Carbon::parse($this->valid_from)->toDateString();
|
||||
$until = Carbon::parse($this->valid_until)->toDateString();
|
||||
if ($from > $until) {
|
||||
// In Laravel, prefer validating in FormRequest.
|
||||
// This keeps your CI behavior by failing the save.
|
||||
throw new \InvalidArgumentException('Valid Until must be the same as or after Valid From.');
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Query helpers
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Find a valid voucher for a student (not expired, active, under max uses, and not used by the student).
|
||||
*
|
||||
* NOTE: Your CI joins discount_usages on u.student_id. That requires discount_usages.student_id to exist.
|
||||
* If your discount_usages table does NOT have student_id (some of your other code uses parent_id),
|
||||
* switch the filter to parent_id instead.
|
||||
*/
|
||||
public function findValidVoucher(string $code, int $studentId)
|
||||
public static function findValidVoucher(string $code, int $studentId): ?self
|
||||
{
|
||||
$code = strtoupper(preg_replace('/\s+/', '', trim($code))); // simple normalize to match stored code
|
||||
$code = strtoupper(preg_replace('/\s+/', '', trim($code)));
|
||||
$today = now()->toDateString();
|
||||
|
||||
$builder = $this->db->table('discount_vouchers v')
|
||||
$q = static::query()
|
||||
->from('discount_vouchers as v')
|
||||
->select('v.*')
|
||||
// Left join usages for this student only
|
||||
->join('discount_usages u', 'u.voucher_id = v.id AND u.student_id = ' . (int) $studentId, 'left')
|
||||
->leftJoin('discount_usages as u', function ($join) use ($studentId) {
|
||||
$join->on('u.voucher_id', '=', 'v.id')
|
||||
->where('u.student_id', '=', (int) $studentId);
|
||||
})
|
||||
->where('v.code', $code)
|
||||
->where('v.is_active', 1)
|
||||
// valid_from is null or <= today
|
||||
->groupStart()
|
||||
->where('v.valid_from IS NULL', null, false)
|
||||
->orWhere('v.valid_from <=', date('Y-m-d'))
|
||||
->groupEnd()
|
||||
->where(function ($w) use ($today) {
|
||||
$w->whereNull('v.valid_from')
|
||||
->orWhere('v.valid_from', '<=', $today);
|
||||
})
|
||||
// valid_until is null or >= today
|
||||
->groupStart()
|
||||
->where('v.valid_until IS NULL', null, false)
|
||||
->orWhere('v.valid_until >=', date('Y-m-d'))
|
||||
->groupEnd()
|
||||
// max_uses is null or times_used < max_uses (use raw to compare two columns)
|
||||
->groupStart()
|
||||
->where('v.max_uses IS NULL', null, false)
|
||||
->orWhere('v.times_used < v.max_uses', null, false)
|
||||
->groupEnd()
|
||||
->where(function ($w) use ($today) {
|
||||
$w->whereNull('v.valid_until')
|
||||
->orWhere('v.valid_until', '>=', $today);
|
||||
})
|
||||
// max_uses is null or times_used < max_uses
|
||||
->where(function ($w) {
|
||||
$w->whereNull('v.max_uses')
|
||||
->orWhereColumn('v.times_used', '<', 'v.max_uses');
|
||||
})
|
||||
// Not previously used by this student (no usage row)
|
||||
->where('u.id IS NULL', null, false);
|
||||
->whereNull('u.id')
|
||||
->limit(1);
|
||||
|
||||
return $builder->get()->getRow();
|
||||
return $q->first();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class EarlyDismissalSignature extends Model
|
||||
class EarlyDismissalSignature extends BaseModel
|
||||
{
|
||||
protected $table = 'early_dismissal_signatures';
|
||||
|
||||
// ✅ CI: timestamps enabled (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'report_date',
|
||||
'school_year',
|
||||
@@ -19,5 +23,18 @@ class EarlyDismissalSignature extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
// report_date is stored as Y-m-d in CI validation; keep as date
|
||||
'report_date' => 'date',
|
||||
|
||||
'file_size' => 'integer',
|
||||
'uploaded_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationship */
|
||||
public function uploader()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'uploaded_by');
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class EmailTemplate extends Model
|
||||
class EmailTemplate extends BaseModel
|
||||
{
|
||||
protected $table = 'email_templates'; // Your CI model only had updated_at (no created_at)
|
||||
protected $table = 'email_templates';
|
||||
|
||||
// CI model didn't specify timestamps; keep off unless your table has created_at/updated_at.
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'variant',
|
||||
@@ -19,31 +22,30 @@ class EmailTemplate extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'updated_by' => 'integer',
|
||||
'updated_at' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch a template by code and variant, falling back to 'default' if needed.
|
||||
* Active only.
|
||||
* Equivalent of CI getActiveTemplates():
|
||||
* is_active=1, orderBy template_key ASC
|
||||
*/
|
||||
public static function getTemplate(string $code, string $variant = 'default'): ?self
|
||||
public static function getActiveTemplates()
|
||||
{
|
||||
// Try exact active
|
||||
$row = static::query()
|
||||
->where('code', $code)
|
||||
->where('variant', $variant)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if ($row) return $row;
|
||||
|
||||
// Fallback to default variant
|
||||
return static::query()
|
||||
->where('code', $code)
|
||||
->where('variant', 'default')
|
||||
->where('is_active', true)
|
||||
->where('is_active', 1)
|
||||
->orderBy('template_key', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI findByKey():
|
||||
* template_key = $key AND is_active=1
|
||||
*/
|
||||
public static function findByKey(string $key): ?self
|
||||
{
|
||||
return static::query()
|
||||
->where('template_key', $key)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ use App\Models\BaseModel;
|
||||
class EmergencyContact extends BaseModel
|
||||
{
|
||||
protected $table = 'emergency_contacts';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'emergency_contact_name',
|
||||
'parent_id',
|
||||
@@ -20,76 +22,75 @@ class EmergencyContact extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'authorized_user_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get emergency contact by student ID.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @return array
|
||||
*/
|
||||
public function getEmergencyContactsByStudentId($studentId)
|
||||
/* Optional relationships */
|
||||
public function parent()
|
||||
{
|
||||
return $this->where('student_id', $studentId)
|
||||
->findAll();
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get emergency contact by parent ID.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @return array
|
||||
*/
|
||||
/*
|
||||
public function getEmergencyContactsByParentId($parentId)
|
||||
public function student()
|
||||
{
|
||||
return $this->groupStart()
|
||||
->where('parent_id', $parentId)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Get authorized user's emergency contact.
|
||||
*
|
||||
* @param int $authorizedUserId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getEmergencyContactByAuthorizedUserId($authorizedUserId)
|
||||
|
||||
public function authorizedUser()
|
||||
{
|
||||
return $this->where('authorized_user_id', $authorizedUserId)
|
||||
return $this->belongsTo(User::class, 'authorized_user_id');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
public static function getEmergencyContactsByStudentId(int $studentId)
|
||||
{
|
||||
return static::query()
|
||||
->where('student_id', $studentId)
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function getEmergencyContactByAuthorizedUserId(int $authorizedUserId): ?self
|
||||
{
|
||||
return static::query()
|
||||
->where('authorized_user_id', $authorizedUserId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all emergency contacts for a specific student and their associated parents or authorized user.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @return array
|
||||
* Your CI logic: (student_id = $studentId) OR (parent_id = $studentId)
|
||||
* Keeping it exactly as-is, even though the 2nd condition looks unusual.
|
||||
*/
|
||||
public function getAllEmergencyContacts($studentId)
|
||||
public static function getAllEmergencyContacts(int $studentId)
|
||||
{
|
||||
return $this->groupStart()
|
||||
->where('student_id', $studentId)
|
||||
->orWhere('parent_id', $studentId)
|
||||
->groupEnd()
|
||||
->findAll();
|
||||
return static::query()
|
||||
->where(function ($q) use ($studentId) {
|
||||
$q->where('student_id', $studentId)
|
||||
->orWhere('parent_id', $studentId);
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
// Function to get the emergency contact based on parent_id
|
||||
public function getEmergencyContactByParentId($parentId)
|
||||
public static function getEmergencyContactByParentId(int $parentId): ?array
|
||||
{
|
||||
// Query to retrieve the emergency contact details using either parent_id or secondparent_id
|
||||
return $this->where('parent_id', $parentId)
|
||||
->select('emergency_contact_name, cellphone')
|
||||
->first(); // Use `first()` to get a single result
|
||||
$row = static::query()
|
||||
->where('parent_id', $parentId)
|
||||
->select(['emergency_contact_name', 'cellphone'])
|
||||
->first();
|
||||
|
||||
return $row ? $row->toArray() : null;
|
||||
}
|
||||
|
||||
public function getEmergencyContactsByParentId($parentId)
|
||||
public static function getEmergencyContactsByParentId(int $parentId)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)->findAll();
|
||||
return static::query()
|
||||
->where('parent_id', $parentId)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-96
@@ -3,11 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Enrollment extends BaseModel
|
||||
{
|
||||
protected $table = 'enrollments';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
@@ -22,125 +26,96 @@ class Enrollment extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
'student_id' => 'required|integer',
|
||||
'class_section_id' => 'permit_empty|integer',
|
||||
'parent_id' => 'required|integer',
|
||||
'school_year' => 'required|string|max_length[25]',
|
||||
'enrollment_date' => 'required|valid_date',
|
||||
'withdrawal_date' => 'permit_empty|valid_date',
|
||||
'is_withdrawn' => 'permit_empty|in_list[0,1]',
|
||||
'enrollment_status' => 'required|in_list[admission under review,payment pending,enrolled,withdraw under review,refund pending,withdrawn]',
|
||||
'admission_status' => 'required|in_list[pending,accepted,denied]',
|
||||
'semester' => 'permit_empty|string|max_length[25]',
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'parent_id' => 'integer',
|
||||
'is_withdrawn' => 'boolean',
|
||||
|
||||
// change to 'datetime' if your columns are DATETIME
|
||||
'enrollment_date' => 'date',
|
||||
'withdrawal_date' => 'date',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'student_id' => [
|
||||
'required' => 'Student ID is required',
|
||||
'integer' => 'Student ID must be an integer',
|
||||
],
|
||||
'class_section_id' => [
|
||||
'integer' => 'Class Section ID must be an integer',
|
||||
],
|
||||
'parent_id' => [
|
||||
'required' => 'Parent ID is required',
|
||||
'integer' => 'Parent ID must be an integer',
|
||||
],
|
||||
'school_year' => [
|
||||
'required' => 'School year is required',
|
||||
'string' => 'School year must be a string',
|
||||
'max_length' => 'School year must not exceed 25 characters',
|
||||
],
|
||||
'enrollment_date' => [
|
||||
'required' => 'Enrollment date is required',
|
||||
'valid_date' => 'Enrollment date must be a valid date',
|
||||
],
|
||||
'withdrawal_date' => [
|
||||
'valid_date' => 'Withdrawal date must be a valid date',
|
||||
],
|
||||
'is_withdrawn' => [
|
||||
'in_list' => 'Withdrawal status must be 0 (not withdrawn) or 1 (withdrawn)',
|
||||
],
|
||||
'enrollment_status' => [
|
||||
'required' => 'Enrollment status is required',
|
||||
'in_list' => 'Enrollment status must be one of: admission under review, payment pending, enrolled, withdraw under review, refund pending, withdrawn',
|
||||
],
|
||||
'admission_status' => [
|
||||
'required' => 'Admission status is required',
|
||||
'in_list' => 'Admission status must be one of: pending, accepted, denied',
|
||||
],
|
||||
'semester' => [
|
||||
'max_length' => 'Semester must not exceed 25 characters',
|
||||
],
|
||||
];
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
protected $skipValidation = false;
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Get all enrolled students (full student info) for a given parent.
|
||||
* Returns a collection of Student rows.
|
||||
*/
|
||||
public function getEnrolledStudents(int $parentId, string $schoolYear = null, string $semester = null): array
|
||||
public static function getEnrolledStudents(int $parentId, ?string $schoolYear = null, ?string $semester = null)
|
||||
{
|
||||
$builder = $this->select('students.*')
|
||||
->join('students', 'students.id = enrollments.student_id')
|
||||
->where('enrollments.parent_id', $parentId);
|
||||
$q = DB::table('enrollments')
|
||||
->join('students', 'students.id', '=', 'enrollments.student_id')
|
||||
->select('students.*')
|
||||
->where('enrollments.parent_id', $parentId);
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('enrollments.school_year', $schoolYear);
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('enrollments.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester) {
|
||||
$builder->where('enrollments.semester', $semester);
|
||||
if (!empty($semester)) {
|
||||
$q->where('enrollments.semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get student basic info (ID, name, grade) for a given parent where status is enrolled or Payment pending.
|
||||
* Get student basic info (ID, name, grade) for a given parent
|
||||
* where status is enrolled or payment pending.
|
||||
*/
|
||||
public function getenrolledStudentDetails(int $parentId, string $schoolYear = null): array
|
||||
public static function getEnrolledStudentDetails(int $parentId, ?string $schoolYear = null): array
|
||||
{
|
||||
$builder = $this->db->table('enrollments')
|
||||
->select('students.id, students.firstname, students.lastname, students.grade_level')
|
||||
->join('students', 'students.id = enrollments.student_id')
|
||||
$q = DB::table('enrollments')
|
||||
->join('students', 'students.id', '=', 'enrollments.student_id')
|
||||
->select('students.id', 'students.firstname', 'students.lastname', 'students.grade_level')
|
||||
->where('enrollments.parent_id', $parentId)
|
||||
->groupStart()
|
||||
->where('enrollments.enrollment_status', 'enrolled')
|
||||
->orWhere('enrollments.enrollment_status', 'payment pending')
|
||||
->groupEnd();
|
||||
->where(function ($w) {
|
||||
$w->where('enrollments.enrollment_status', 'enrolled')
|
||||
->orWhere('enrollments.enrollment_status', 'payment pending');
|
||||
});
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('enrollments.school_year', $schoolYear);
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('enrollments.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
return $q->get()->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest enrollment_status for a student in a given school year.
|
||||
*
|
||||
* @param int $studentId
|
||||
* @param string $schoolYear e.g. "2025-2026"
|
||||
* @return string|null e.g. "enrolled", "pending", "denied", or null if not found
|
||||
*/
|
||||
public function getEnrollmentStatus(int $studentId, string $schoolYear): ?string
|
||||
{
|
||||
$row = $this->select('enrollment_status')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
// prefer most recently updated, then most recent enrollment_date, then newest id
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->orderBy('enrollment_date', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
* Get the latest enrollment_status for a student in a given school year.
|
||||
*/
|
||||
public static function getEnrollmentStatus(int $studentId, string $schoolYear): ?string
|
||||
{
|
||||
$row = static::query()
|
||||
->select('enrollment_status')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('enrollment_date')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
return $row['enrollment_status'] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
return $row?->enrollment_status;
|
||||
}
|
||||
}
|
||||
+122
-130
@@ -7,7 +7,10 @@ use App\Models\BaseModel;
|
||||
class Event extends BaseModel
|
||||
{
|
||||
protected $table = 'events';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: timestamps enabled (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'event_name',
|
||||
'event_category',
|
||||
@@ -21,164 +24,153 @@ class Event extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $casts = [
|
||||
'amount' => 'decimal:2', // adjust precision if needed
|
||||
'expiration_date' => 'date', // change to 'datetime' if stored as DATETIME
|
||||
'created_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function charges()
|
||||
{
|
||||
return $this->hasMany(EventCharge::class, 'event_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Get all upcoming events (not expired).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUpcomingEvents($schoolYear = null)
|
||||
{
|
||||
$builder = $this->where('expiration_date >=', date('Y-m-d'));
|
||||
public static function getUpcomingEvents(?string $schoolYear = null)
|
||||
{
|
||||
$q = static::query()
|
||||
->whereDate('expiration_date', '>=', now()->toDateString());
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $q->orderBy('expiration_date', 'asc')->get();
|
||||
}
|
||||
|
||||
return $builder->orderBy('expiration_date', 'ASC')->findAll();
|
||||
}
|
||||
/**
|
||||
* Get events by name (partial match).
|
||||
*/
|
||||
public static function getEventsByName(string $name, ?string $schoolYear = null)
|
||||
{
|
||||
$q = static::query()
|
||||
->where('event_name', 'like', '%' . $name . '%');
|
||||
|
||||
/**
|
||||
* Get events by name (partial match).
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null $schoolYear
|
||||
* @return array
|
||||
*/
|
||||
public function getEventsByName(string $name, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->like('event_name', $name);
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
return $q->orderBy('expiration_date', 'asc')->get();
|
||||
}
|
||||
|
||||
return $builder->orderBy('expiration_date', 'ASC')->findAll();
|
||||
}
|
||||
/**
|
||||
* Get all events (optionally filtered by school year).
|
||||
*/
|
||||
public static function getAllEvents(?string $schoolYear = null)
|
||||
{
|
||||
$q = static::query();
|
||||
|
||||
/**
|
||||
* Get all events.
|
||||
*
|
||||
* @param string|null $schoolYear
|
||||
* @return array
|
||||
*/
|
||||
public function getAllEvents($schoolYear = null)
|
||||
{
|
||||
$builder = $this;
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder = $builder->where('school_year', $schoolYear);
|
||||
return $q->orderByDesc('created_at')->get();
|
||||
}
|
||||
|
||||
return $builder->orderBy('created_at', 'DESC')->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new event.
|
||||
*
|
||||
* @param array $data
|
||||
* @return int|string|false
|
||||
*/
|
||||
public function addEvent(array $data)
|
||||
{
|
||||
return $this->insert($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by expiration date.
|
||||
*
|
||||
* @param string $date
|
||||
* @param string|null $schoolYear
|
||||
* @return array
|
||||
*/
|
||||
public function getEventsByDate($date, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->where('expiration_date', $date);
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
/**
|
||||
* Add a new event. Returns inserted id.
|
||||
*/
|
||||
public static function addEvent(array $data): int
|
||||
{
|
||||
$row = static::create($data);
|
||||
return (int) $row->id;
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
/**
|
||||
* Get events by expiration date.
|
||||
*/
|
||||
public static function getEventsByDate(string $date, ?string $schoolYear = null)
|
||||
{
|
||||
$q = static::query()->whereDate('expiration_date', $date);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active (not expired) events.
|
||||
*
|
||||
* @param string|null $schoolYear
|
||||
* @param string|null $semester
|
||||
* @return array
|
||||
*/
|
||||
public function getActiveEvents($schoolYear = null, $semester = null)
|
||||
{
|
||||
$builder = $this->where('expiration_date >=', date('Y-m-d'));
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
if ($semester) {
|
||||
$builder->where('semester', $semester);
|
||||
/**
|
||||
* Get all active (not expired) events (optionally filtered by year/semester).
|
||||
*/
|
||||
public static function getActiveEvents(?string $schoolYear = null, ?string $semester = null)
|
||||
{
|
||||
$q = static::query()
|
||||
->whereDate('expiration_date', '>=', now()->toDateString());
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if (!empty($semester)) {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
|
||||
return $q->orderBy('expiration_date', 'asc')->get();
|
||||
}
|
||||
|
||||
return $builder->orderBy('expiration_date', 'ASC')->findAll();
|
||||
}
|
||||
/**
|
||||
* Get event by id (optionally filtered by school year).
|
||||
*/
|
||||
public static function getEvent(int $id, ?string $schoolYear = null): ?self
|
||||
{
|
||||
$q = static::query()->whereKey($id);
|
||||
|
||||
/**
|
||||
* Get event by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $schoolYear
|
||||
* @return array|null
|
||||
*/
|
||||
public function getEvent($id, $schoolYear = null)
|
||||
{
|
||||
$builder = $this->where('id', $id);
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
return $q->first();
|
||||
}
|
||||
|
||||
return $builder->first();
|
||||
}
|
||||
/**
|
||||
* Update event by id (optionally constrained to a school year).
|
||||
*/
|
||||
public static function updateEvent(int $id, array $data, ?string $schoolYear = null): bool
|
||||
{
|
||||
$q = static::query()->whereKey($id);
|
||||
|
||||
/**
|
||||
* Update event by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @param string|null $schoolYear
|
||||
* @return bool
|
||||
*/
|
||||
public function updateEvent($id, $data, $schoolYear = null)
|
||||
{
|
||||
if ($schoolYear) {
|
||||
// Ensure only updates for this school year
|
||||
$this->where('id', $id)->where('school_year', $schoolYear)->set($data)->update();
|
||||
return true;
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $q->update($data) >= 0;
|
||||
}
|
||||
|
||||
return $this->update($id, $data);
|
||||
}
|
||||
/**
|
||||
* Delete event by id (optionally constrained to a school year).
|
||||
*/
|
||||
public static function deleteEvent(int $id, ?string $schoolYear = null): bool
|
||||
{
|
||||
$q = static::query()->whereKey($id);
|
||||
|
||||
/**
|
||||
* Delete event by ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $schoolYear
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteEvent($id, $schoolYear = null)
|
||||
{
|
||||
if ($schoolYear) {
|
||||
return $this->where('id', $id)->where('school_year', $schoolYear)->delete();
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return (bool) $q->delete();
|
||||
}
|
||||
|
||||
return $this->delete($id);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class EventCharge extends EventCharges
|
||||
{
|
||||
}
|
||||
+50
-21
@@ -3,11 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EventCharges extends BaseModel
|
||||
{
|
||||
protected $table = 'event_charges';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'event_id',
|
||||
'parent_id',
|
||||
@@ -20,29 +24,54 @@ class EventCharges extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
public function getChargesWithEventInfo($parentId = null, $schoolYear = null, $semester = null)
|
||||
protected $casts = [
|
||||
'event_id' => 'integer',
|
||||
'parent_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'participation' => 'boolean', // change if stored as string/enum
|
||||
'charged' => 'boolean', // change if stored as string/enum
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function event()
|
||||
{
|
||||
$builder = $this->select('event_charges.*, events.event_name, events.amount')
|
||||
->join('events', 'events.id = event_charges.event_id', 'left');
|
||||
|
||||
if ($parentId) {
|
||||
$builder->where('event_charges.parent_id', $parentId);
|
||||
}
|
||||
|
||||
if ($schoolYear) {
|
||||
$builder->where('event_charges.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if ($semester) {
|
||||
$builder->where('event_charges.semester', $semester);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
return $this->belongsTo(Event::class, 'event_id');
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getChargesWithEventInfo()
|
||||
* Returns rows with: event_charges.*, events.event_name, events.amount
|
||||
*/
|
||||
public static function getChargesWithEventInfo(?int $parentId = null, ?string $schoolYear = null, ?string $semester = null): array
|
||||
{
|
||||
$q = DB::table('event_charges')
|
||||
->leftJoin('events', 'events.id', '=', 'event_charges.event_id')
|
||||
->select('event_charges.*', 'events.event_name', 'events.amount');
|
||||
|
||||
if (!empty($parentId)) {
|
||||
$q->where('event_charges.parent_id', $parentId);
|
||||
}
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('event_charges.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
if (!empty($semester)) {
|
||||
$q->where('event_charges.semester', $semester);
|
||||
}
|
||||
|
||||
return $q->get()->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
}
|
||||
|
||||
+30
-3
@@ -2,11 +2,22 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class Exam extends Model
|
||||
class Exam extends BaseModel
|
||||
{
|
||||
protected $table = 'exams';
|
||||
|
||||
/**
|
||||
* Your CI model only uses created_at (updatedField = '').
|
||||
* In Laravel, easiest is to disable timestamps and manage created_at yourself OR
|
||||
* enable timestamps but disable UPDATED_AT.
|
||||
*
|
||||
* We'll keep auto created_at, no updated_at:
|
||||
*/
|
||||
public $timestamps = false;
|
||||
const UPDATED_AT = null; // ✅ no updated_at column
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'student_school_id',
|
||||
@@ -14,5 +25,21 @@ class Exam extends Model
|
||||
'exam_name',
|
||||
'created_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ExamDraft extends Model
|
||||
class ExamDraft extends BaseModel
|
||||
{
|
||||
protected $table = 'exam_drafts';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'teacher_id',
|
||||
'class_section_id',
|
||||
@@ -29,5 +33,58 @@ class ExamDraft extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'teacher_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'admin_id' => 'integer',
|
||||
'is_legacy' => 'boolean',
|
||||
|
||||
'version' => 'integer',
|
||||
'previous_draft_id' => 'integer',
|
||||
|
||||
'reviewed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* CI had updateOnlyChanged=false (force updates even if nothing changed).
|
||||
* In Laravel, you can force a save by calling:
|
||||
* $model->touch(); // updates updated_at only
|
||||
* or
|
||||
* $model->save(['timestamps' => true]); // normal save
|
||||
*
|
||||
* If you want a helper:
|
||||
*/
|
||||
public function forceSave(array $attributes = []): bool
|
||||
{
|
||||
if (!empty($attributes)) {
|
||||
$this->fill($attributes);
|
||||
}
|
||||
|
||||
// Force updated_at refresh even if no attributes changed
|
||||
$this->updated_at = now();
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/* Optional relationships */
|
||||
public function teacher()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'teacher_id');
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
public function previousDraft()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'previous_draft_id');
|
||||
}
|
||||
}
|
||||
+62
-27
@@ -3,11 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Expense extends BaseModel
|
||||
{
|
||||
protected $table = 'expenses';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'category',
|
||||
'amount',
|
||||
@@ -27,44 +31,75 @@ class Expense extends BaseModel
|
||||
'approved_by',
|
||||
'reimbursement_id',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $validationRules = [
|
||||
// Add your validation rules here as needed
|
||||
protected $casts = [
|
||||
'amount' => 'decimal:2', // adjust precision if needed
|
||||
'date_of_purchase'=> 'date', // change to 'datetime' if stored as DATETIME
|
||||
|
||||
'purchased_by' => 'integer',
|
||||
'added_by' => 'integer',
|
||||
'reimbursement_id'=> 'integer',
|
||||
'approved_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
public function getReimbursedExpensesWithDetails(array $filters = [])
|
||||
/* Optional relationships */
|
||||
public function reimbursement()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('expenses');
|
||||
$builder->select('expenses.*,
|
||||
r.id AS reimb_id,
|
||||
r.amount AS reimb_amount, r.reimbursement_method, r.check_number,
|
||||
r.receipt_path AS reimb_receipt, r.status AS reimb_status,
|
||||
r.school_year, r.semester, r.created_at AS reimb_created_at,
|
||||
reimb.firstname AS reimb_firstname, reimb.lastname AS reimb_lastname,
|
||||
approver.firstname AS approver_firstname, approver.lastname AS approver_lastname');
|
||||
$builder->join('reimbursements r', 'r.expense_id = expenses.id', 'inner');
|
||||
$builder->join('users reimb', 'reimb.id = r.reimbursed_to', 'left');
|
||||
$builder->join('users approver', 'approver.id = r.approved_by', 'left');
|
||||
return $this->hasOne(Reimbursement::class, 'expense_id');
|
||||
}
|
||||
|
||||
public function purchaser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'purchased_by');
|
||||
}
|
||||
|
||||
public function addedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'added_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel equivalent of getReimbursedExpensesWithDetails().
|
||||
*
|
||||
* Returns a Query\Builder (so you can add paginate(), get(), etc.)
|
||||
*
|
||||
* Filters supported:
|
||||
* - school_year
|
||||
* - semester
|
||||
* - status
|
||||
* - user_id (reimbursed_to)
|
||||
*/
|
||||
public static function getReimbursedExpensesWithDetails(array $filters = [])
|
||||
{
|
||||
$q = DB::table('expenses')
|
||||
->selectRaw("
|
||||
expenses.*,
|
||||
r.id AS reimb_id,
|
||||
r.amount AS reimb_amount, r.reimbursement_method, r.check_number,
|
||||
r.receipt_path AS reimb_receipt, r.status AS reimb_status,
|
||||
r.school_year, r.semester, r.created_at AS reimb_created_at,
|
||||
r.reimbursed_to AS reimb_recipient_id,
|
||||
reimb.firstname AS reimb_firstname, reimb.lastname AS reimb_lastname,
|
||||
approver.firstname AS approver_firstname, approver.lastname AS approver_lastname
|
||||
")
|
||||
->join('reimbursements as r', 'r.expense_id', '=', 'expenses.id')
|
||||
->leftJoin('users as reimb', 'reimb.id', '=', 'r.reimbursed_to')
|
||||
->leftJoin('users as approver', 'approver.id', '=', 'r.approved_by');
|
||||
|
||||
if (!empty($filters['school_year'])) {
|
||||
$builder->where('r.school_year', $filters['school_year']);
|
||||
$q->where('r.school_year', $filters['school_year']);
|
||||
}
|
||||
if (!empty($filters['semester'])) {
|
||||
$builder->where('r.semester', $filters['semester']);
|
||||
$q->where('r.semester', $filters['semester']);
|
||||
}
|
||||
if (!empty($filters['status'])) {
|
||||
$builder->where('r.status', $filters['status']);
|
||||
$q->where('r.status', $filters['status']);
|
||||
}
|
||||
if (!empty($filters['user_id'])) {
|
||||
$builder->where('r.reimbursed_to', $filters['user_id']);
|
||||
$q->where('r.reimbursed_to', $filters['user_id']);
|
||||
}
|
||||
|
||||
$builder->orderBy('r.created_at', 'DESC');
|
||||
|
||||
return $builder;
|
||||
return $q->orderByDesc('r.created_at');
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-3
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class Family extends Model
|
||||
class Family extends BaseModel
|
||||
{
|
||||
protected $table = 'families';
|
||||
|
||||
// CI model didn't specify timestamps
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'family_code',
|
||||
'household_name',
|
||||
@@ -23,5 +27,19 @@ class Family extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function guardians()
|
||||
{
|
||||
return $this->hasMany(FamilyGuardian::class, 'family_id');
|
||||
}
|
||||
|
||||
public function commPrefs()
|
||||
{
|
||||
return $this->hasMany(FamilyCommPref::class, 'family_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class FamilyCommPref extends Model
|
||||
class FamilyCommPref extends BaseModel
|
||||
{
|
||||
protected $table = 'family_comm_prefs';
|
||||
|
||||
// CI model didn't specify timestamps
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'family_id',
|
||||
'category',
|
||||
@@ -16,5 +20,17 @@ class FamilyCommPref extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'family_id' => 'integer',
|
||||
'via_email' => 'boolean',
|
||||
'via_sms' => 'boolean',
|
||||
'cc_all_guardians' => 'boolean',
|
||||
];
|
||||
|
||||
// Optional relationship
|
||||
public function family()
|
||||
{
|
||||
return $this->belongsTo(Family::class, 'family_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class FamilyGuardian extends Model
|
||||
class FamilyGuardian extends BaseModel
|
||||
{
|
||||
protected $table = 'family_guardians';
|
||||
|
||||
// CI model didn't specify timestamps
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'family_id',
|
||||
'user_id',
|
||||
@@ -18,5 +22,23 @@ class FamilyGuardian extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'family_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'is_primary' => 'boolean',
|
||||
'receive_emails' => 'boolean',
|
||||
'receive_sms' => 'boolean',
|
||||
];
|
||||
|
||||
// Optional relationships
|
||||
public function family()
|
||||
{
|
||||
return $this->belongsTo(Family::class, 'family_id');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class FamilyStudent extends BaseModel {
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FamilyStudent extends BaseModel
|
||||
{
|
||||
protected $table = 'family_students';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// CI model didn't specify timestamps
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'family_id',
|
||||
'student_id',
|
||||
@@ -14,11 +20,39 @@ class FamilyStudent extends BaseModel {
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
public function getFamiliesForStudent(int $studentId): array {
|
||||
$db = \Config\Database::connect();
|
||||
$sql = "SELECT f.* , fs.is_primary_home\n FROM family_students fs\n JOIN families f ON f.id = fs.family_id\n WHERE fs.student_id = ? AND f.is_active = 1\n ORDER BY fs.is_primary_home DESC, f.household_name";
|
||||
return $db->query($sql, [$studentId])->getResultArray();
|
||||
protected $casts = [
|
||||
'family_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'is_primary_home' => 'boolean',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function family()
|
||||
{
|
||||
return $this->belongsTo(Family::class, 'family_id');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel equivalent of CI getFamiliesForStudent().
|
||||
* Returns array rows: families.* + is_primary_home
|
||||
*/
|
||||
public static function getFamiliesForStudent(int $studentId): array
|
||||
{
|
||||
return DB::table('family_students as fs')
|
||||
->join('families as f', 'f.id', '=', 'fs.family_id')
|
||||
->where('fs.student_id', $studentId)
|
||||
->where('f.is_active', 1)
|
||||
->orderByDesc('fs.is_primary_home')
|
||||
->orderBy('f.household_name')
|
||||
->select('f.*', 'fs.is_primary_home')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
}
|
||||
+50
-42
@@ -7,10 +7,13 @@ use App\Models\BaseModel;
|
||||
class FinalExam extends BaseModel
|
||||
{
|
||||
protected $table = 'final_exam';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
|
||||
/**
|
||||
* CI: useTimestamps = false (even though table has created_at/updated_at fields).
|
||||
* So we keep timestamps OFF and you can set created_at/updated_at manually if needed.
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
@@ -23,46 +26,51 @@ class FinalExam extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
|
||||
protected $casts = [];
|
||||
protected $castHandlers = [];
|
||||
// score is often numeric; adjust if yours is integer
|
||||
'score' => 'decimal:2',
|
||||
|
||||
// Dates
|
||||
public $timestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
public function getFinalExamScore($studentId, string $semester, $schoolYear)
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
// Query the final_score table for the final exam score based on student ID, semester, and school year
|
||||
$result = $this->select('score') // We only need the score
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first(); // Get the first matching result
|
||||
|
||||
// Return the score, or null if no result is found
|
||||
return $result ? $result['score'] : null;
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getFinalExamScore()
|
||||
*/
|
||||
public static function getFinalExamScore(
|
||||
int $studentId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $classSectionId = null
|
||||
) {
|
||||
$q = static::query()
|
||||
->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$q->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
$row = $q->first();
|
||||
|
||||
return $row?->score;
|
||||
}
|
||||
}
|
||||
+40
-13
@@ -7,8 +7,11 @@ use App\Models\BaseModel;
|
||||
class FinalScore extends BaseModel
|
||||
{
|
||||
protected $table = 'final_score';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
// CI model didn't specify timestamps behavior; table includes created_at/updated_at.
|
||||
// If you want Laravel to auto-manage them, keep true (recommended).
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
@@ -22,19 +25,43 @@ class FinalScore extends BaseModel
|
||||
'updated_at',
|
||||
'semester',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
// Function to get the final exam score for a specific student, semester, and school year
|
||||
public function getFinalExamScore($studentId, $semester, $schoolYear)
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'teacher_id' => 'integer',
|
||||
'score' => 'decimal:2', // change to 'integer' if score is whole number
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
// Query the final_score table for the final exam score based on student ID, semester, and school year
|
||||
$result = $this->select('score') // We only need the score
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first(); // Get the first matching result
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
// Return the score, or null if no result is found
|
||||
return $result ? $result['score'] : null;
|
||||
public function teacher()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'teacher_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getFinalExamScore()
|
||||
*/
|
||||
public static function getFinalExamScore(int $studentId, string $semester, string $schoolYear)
|
||||
{
|
||||
$row = static::query()
|
||||
->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
|
||||
return $row?->score;
|
||||
}
|
||||
}
|
||||
+35
-3
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class Flag extends Model
|
||||
class Flag extends BaseModel
|
||||
{
|
||||
protected $table = 'flag';
|
||||
|
||||
// Table contains created_at/updated_at; enable auto timestamps (recommended).
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'student_name',
|
||||
@@ -26,5 +30,33 @@ class Flag extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'updated_by_open' => 'integer',
|
||||
'updated_by_closed' => 'integer',
|
||||
'updated_by_canceled' => 'integer',
|
||||
'flag_datetime' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function openedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by_open');
|
||||
}
|
||||
|
||||
public function closedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by_closed');
|
||||
}
|
||||
|
||||
public function canceledBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by_canceled');
|
||||
}
|
||||
}
|
||||
+49
-10
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class GradingLock extends Model
|
||||
class GradingLock extends BaseModel
|
||||
{
|
||||
protected $table = 'grading_locks';
|
||||
|
||||
// ✅ CI: useTimestamps = true
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'class_section_id',
|
||||
'semester',
|
||||
@@ -17,13 +21,48 @@ class GradingLock extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
/**
|
||||
* TODO: Manual port review required for custom CI query methods from GradingLock.
|
||||
* - getLock()
|
||||
* - isLocked()
|
||||
*
|
||||
* This automated conversion preserves schema metadata only.
|
||||
*/
|
||||
protected $casts = [
|
||||
'class_section_id' => 'integer',
|
||||
'is_locked' => 'boolean',
|
||||
'locked_by' => 'integer',
|
||||
'locked_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
public function lockedByUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'locked_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getLock()
|
||||
*/
|
||||
public static function getLock(int $classSectionId, string $semester, string $schoolYear): ?self
|
||||
{
|
||||
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI isLocked()
|
||||
*/
|
||||
public static function isLocked(int $classSectionId, string $semester, string $schoolYear): bool
|
||||
{
|
||||
$row = static::getLock($classSectionId, $semester, $schoolYear);
|
||||
return (bool) ($row?->is_locked ?? false);
|
||||
}
|
||||
}
|
||||
+69
-50
@@ -3,14 +3,18 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Homework extends BaseModel
|
||||
{
|
||||
protected $table = 'homework';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
|
||||
/**
|
||||
* CI: useTimestamps = false (even though created_at/updated_at columns exist).
|
||||
* Keep it OFF to match behavior. If you want Laravel to auto-manage, tell me.
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
@@ -25,60 +29,75 @@ class Homework extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'homework_index' => 'integer',
|
||||
|
||||
protected $casts = [];
|
||||
protected $castHandlers = [];
|
||||
// keep numeric; change to 'integer' if your scores are whole numbers
|
||||
'score' => 'decimal:2',
|
||||
|
||||
// Dates
|
||||
public $timestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the average homework score for a student
|
||||
*
|
||||
* @param int $studentId The student ID
|
||||
* @param string $semester The semester (e.g., 'Fall', 'Spring')
|
||||
* @param string $schoolYear The school year (e.g., '2023-2024')
|
||||
* @return float The average score (0.0 if no homework found)
|
||||
*/
|
||||
public function getAverageHomeworkScore(int $studentId, string $semester, string $schoolYear): float
|
||||
{
|
||||
// Build the query using Query Builder
|
||||
$builder = $this->db->table('homework');
|
||||
$builder->selectAvg('score', 'average_score')
|
||||
/**
|
||||
* Laravel equivalent of CI getAverageHomeworkScore()
|
||||
* Computes average based only on non-blank numeric scores.
|
||||
*/
|
||||
public static function getAverageHomeworkScore(
|
||||
int $studentId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $classSectionId = null
|
||||
): ?float {
|
||||
$q = DB::table('homework')
|
||||
->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('score IS NOT NULL'); // Only include records with actual scores
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
// Execute the query
|
||||
$result = $builder->get()->getRow();
|
||||
if ($classSectionId !== null) {
|
||||
$q->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
// Return the average or 0.0 if no results
|
||||
return $result ? (float)$result->average_score : 0.0;
|
||||
}
|
||||
$rows = $q->get();
|
||||
if ($rows->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$total = 0.0;
|
||||
$count = 0;
|
||||
|
||||
}
|
||||
foreach ($rows as $r) {
|
||||
$score = $r->score ?? null;
|
||||
|
||||
if ($score === null) continue;
|
||||
if (is_string($score) && trim($score) === '') continue;
|
||||
if ($score === '') continue;
|
||||
if (!is_numeric($score)) continue;
|
||||
|
||||
$total += (float) $score;
|
||||
$count++;
|
||||
}
|
||||
|
||||
if ($count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round($total / $count, 2);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class InventoryCategory extends Model
|
||||
class InventoryCategory extends BaseModel
|
||||
{
|
||||
protected $table = 'inventory_categories';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'name',
|
||||
@@ -16,12 +20,23 @@ class InventoryCategory extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
/**
|
||||
* TODO: Manual port review required for custom CI query methods from InventoryCategory.
|
||||
* - optionsForType()
|
||||
*
|
||||
* This automated conversion preserves schema metadata only.
|
||||
*/
|
||||
protected $casts = [
|
||||
'grade_min' => 'integer',
|
||||
'grade_max' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Equivalent of CI optionsForType()
|
||||
* Returns array rows: id, name, description, grade_min, grade_max
|
||||
*/
|
||||
public static function optionsForType(string $type): array
|
||||
{
|
||||
return static::query()
|
||||
->select(['id', 'name', 'description', 'grade_min', 'grade_max'])
|
||||
->where('type', $type)
|
||||
->orderBy('name', 'asc')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,10 @@ use App\Models\BaseModel;
|
||||
class InventoryItem extends BaseModel
|
||||
{
|
||||
protected $table = 'inventory_items';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useSoftDeletes = false;
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
// IMPORTANT: include everything you ever update/insert
|
||||
// ✅ CI: useTimestamps = true
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'category_id',
|
||||
@@ -37,13 +34,20 @@ class InventoryItem extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'category_id' => 'integer',
|
||||
'quantity' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
|
||||
protected $validationRules = [
|
||||
'type' => 'required|in_list[classroom,book,office,kitchen]',
|
||||
'name' => 'required|min_length[2]',
|
||||
'quantity' => 'permit_empty|integer',
|
||||
'unit_price' => 'permit_empty|decimal',
|
||||
'semester' => 'permit_empty|in_list[Spring,Fall]',
|
||||
'school_year' => 'permit_empty|max_length[16]', // e.g. 2025-2026
|
||||
'good_qty' => 'integer',
|
||||
'needs_repair_qty' => 'integer',
|
||||
'need_replace_qty' => 'integer',
|
||||
'cannot_find_qty' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/* Optional relationships */
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(InventoryCategory::class, 'category_id');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class InventoryMovement extends Model
|
||||
class InventoryMovement extends BaseModel
|
||||
{
|
||||
protected $table = 'inventory_movements';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'item_id',
|
||||
'qty_change',
|
||||
@@ -22,5 +26,39 @@ class InventoryMovement extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'item_id' => 'integer',
|
||||
'qty_change' => 'integer',
|
||||
'performed_by' => 'integer',
|
||||
'teacher_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'class_section_id'=> 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function item()
|
||||
{
|
||||
return $this->belongsTo(InventoryItem::class, 'item_id');
|
||||
}
|
||||
|
||||
public function performer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'performed_by');
|
||||
}
|
||||
|
||||
public function teacher()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'teacher_id');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
+220
-195
@@ -3,11 +3,19 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Invoice extends BaseModel
|
||||
{
|
||||
protected $table = 'invoices';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* CI: useTimestamps = false because DB defaults/triggers manage created_at/updated_at.
|
||||
* In Laravel, keep timestamps OFF and let DB handle them.
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'invoice_number',
|
||||
@@ -26,273 +34,290 @@ class Invoice extends BaseModel
|
||||
'semester',
|
||||
];
|
||||
|
||||
// Your table handles timestamps (defaults/triggers), keep CI auto off
|
||||
public $timestamps = true;
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'has_discount' => 'boolean',
|
||||
|
||||
// --- Validate as DATETIME strings like "YYYY-MM-DD HH:MM:SS" ---
|
||||
protected $validationRules = [
|
||||
// allow update without tripping uniqueness
|
||||
'invoice_number' => 'required|is_unique[invoices.invoice_number,id,{id}]',
|
||||
'total_amount' => 'required|decimal',
|
||||
'balance' => 'permit_empty|decimal',
|
||||
'issue_date' => 'required|valid_date[Y-m-d H:i:s]',
|
||||
'due_date' => 'permit_empty|valid_date[Y-m-d H:i:s]',
|
||||
'status' => 'required|string|max_length[50]',
|
||||
];
|
||||
'total_amount' => 'decimal:2',
|
||||
'balance' => 'decimal:2',
|
||||
'paid_amount' => 'decimal:2',
|
||||
|
||||
|
||||
protected $validationMessages = [
|
||||
'invoice_number' => [
|
||||
'required' => 'The invoice number is required.',
|
||||
'is_unique' => 'The invoice number must be unique.',
|
||||
],
|
||||
'total_amount' => [
|
||||
'required' => 'The total amount is required.',
|
||||
'decimal' => 'The total amount must be a valid decimal.',
|
||||
],
|
||||
'balance' => [
|
||||
'decimal' => 'The balance must be a valid decimal.',
|
||||
],
|
||||
'issue_date' => [
|
||||
'required' => 'The issue date is required.',
|
||||
'valid_date' => 'The issue date must be a valid datetime (YYYY-MM-DD HH:MM:SS).',
|
||||
],
|
||||
'due_date' => [
|
||||
'valid_date' => 'The due date must be a valid datetime (YYYY-MM-DD HH:MM:SS).',
|
||||
],
|
||||
'status' => [
|
||||
'required' => 'The status is required.',
|
||||
'max_length' => 'The status must not exceed 50 characters.',
|
||||
],
|
||||
'issue_date' => 'datetime',
|
||||
'due_date' => 'datetime',
|
||||
// created_at/updated_at exist but are DB-managed; you can still cast if you like:
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
// --- Normalize any incoming date/datetime into UTC 'Y-m-d H:i:s' ---
|
||||
protected $beforeInsert = ['normalizeDateTimes'];
|
||||
protected $beforeUpdate = ['normalizeDateTimes'];
|
||||
|
||||
protected function normalizeDateTimes(array $data): array
|
||||
/* Optional relationships */
|
||||
public function parent()
|
||||
{
|
||||
foreach (['issue_date', 'due_date', 'created_at', 'updated_at'] as $field) {
|
||||
if (array_key_exists($field, $data['data'])) {
|
||||
$val = $data['data'][$field];
|
||||
if ($val === null || $val === '' || $val === '0000-00-00' || $val === '0000-00-00 00:00:00') {
|
||||
// keep NULL/empty for nullable fields
|
||||
$data['data'][$field] = ($field === 'due_date') ? null : $val;
|
||||
continue;
|
||||
}
|
||||
$normalized = $this->toUtcDateTimeString($val);
|
||||
if ($normalized !== null) {
|
||||
$data['data'][$field] = $normalized;
|
||||
}
|
||||
// else leave as-is; validation will catch invalid format
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
private function toUtcDateTimeString($raw): ?string
|
||||
public function events()
|
||||
{
|
||||
return $this->hasMany(InvoiceEvent::class, 'invoice_id');
|
||||
}
|
||||
|
||||
public function discountUsages()
|
||||
{
|
||||
return $this->hasMany(DiscountUsage::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI beforeInsert/beforeUpdate equivalents (normalize to UTC)
|
||||
* ========================= */
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function (self $m) {
|
||||
$m->normalizeDateTimes();
|
||||
});
|
||||
|
||||
static::updating(function (self $m) {
|
||||
$m->normalizeDateTimes();
|
||||
});
|
||||
}
|
||||
|
||||
private function normalizeDateTimes(): void
|
||||
{
|
||||
foreach (['issue_date', 'due_date', 'created_at', 'updated_at'] as $field) {
|
||||
if (!$this->isDirty($field)) continue;
|
||||
|
||||
$val = $this->{$field};
|
||||
|
||||
if ($val === null || $val === '' || $val === '0000-00-00' || $val === '0000-00-00 00:00:00') {
|
||||
// keep NULL for due_date; keep other values as-is (DB defaults/triggers)
|
||||
if ($field === 'due_date') $this->{$field} = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized = static::toUtcDateTimeString($val);
|
||||
if ($normalized !== null) {
|
||||
$this->{$field} = $normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function toUtcDateTimeString($raw): ?string
|
||||
{
|
||||
if ($raw instanceof \DateTimeInterface) {
|
||||
$dt = (new \DateTimeImmutable('@' . $raw->getTimestamp()))
|
||||
->setTimezone(new \DateTimeZone('UTC'));
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
return Carbon::instance($raw)->utc()->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$s = trim((string) $raw);
|
||||
if ($s === '') return null;
|
||||
|
||||
// Try strict formats first
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
|
||||
$dt = \DateTime::createFromFormat('Y-m-d H:i:s', $s, $utc);
|
||||
if ($dt !== false) {
|
||||
$dt->setTimezone($utc);
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$d = \DateTime::createFromFormat('Y-m-d', $s, $utc);
|
||||
if ($d !== false) {
|
||||
// If date-only sneaks in, store at 12:00:00 to avoid boundary confusion
|
||||
$d->setTime(12, 0, 0);
|
||||
$d->setTimezone($utc);
|
||||
return $d->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
// Fallback parser (may accept ISO8601, etc.)
|
||||
// strict datetime
|
||||
try {
|
||||
$flex = new \DateTime($s);
|
||||
$flex->setTimezone($utc);
|
||||
return $flex->format('Y-m-d H:i:s');
|
||||
$dt = Carbon::createFromFormat('Y-m-d H:i:s', $s, 'UTC');
|
||||
return $dt->utc()->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// date-only sneaks in => store at 12:00:00
|
||||
try {
|
||||
$d = Carbon::createFromFormat('Y-m-d', $s, 'UTC')->setTime(12, 0, 0);
|
||||
return $d->utc()->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// fallback parser (ISO8601 etc.)
|
||||
try {
|
||||
return Carbon::parse($s)->utc()->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------- Queries -----------------------
|
||||
/* =========================
|
||||
* Queries (ported from CI)
|
||||
* ========================= */
|
||||
|
||||
public function getInvoicesByUserId($userId, $schoolYear = null)
|
||||
public static function getInvoicesByUserId(int $userId, ?string $schoolYear = null): array
|
||||
{
|
||||
$builder = $this->where('parent_id', $userId);
|
||||
$q = DB::table('invoices as i')
|
||||
->select('i.*')
|
||||
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
|
||||
->where('i.parent_id', $userId);
|
||||
|
||||
if ($schoolYear !== null) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
$q->where('i.school_year', $schoolYear);
|
||||
}
|
||||
return $builder->findAll();
|
||||
|
||||
return $q->get()->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
|
||||
public function updateInvoiceStatus($invoiceId, $status)
|
||||
public static function updateInvoiceStatus(int $invoiceId, string $status): bool
|
||||
{
|
||||
return $this->update($invoiceId, ['status' => $status]);
|
||||
return DB::table('invoices')->where('id', $invoiceId)->update(['status' => $status]) >= 0;
|
||||
}
|
||||
|
||||
public function getUnpaidInvoices()
|
||||
public static function getUnpaidInvoices(): array
|
||||
{
|
||||
return $this->where('status', 'unpaid')
|
||||
return DB::table('invoices')
|
||||
->whereIn('status', ['Unpaid', 'Partially Paid', 'unpaid', 'partially paid'])
|
||||
->orderBy('due_date', 'ASC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
public function getLatestInvoiceTotalAmount($parentId)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('total_amount')
|
||||
->get()
|
||||
->getRow('total_amount');
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function getLatestInvoicePaidAmount($parentId)
|
||||
public static function getLatestInvoiceTotalAmount(int $parentId)
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('paid_amount')
|
||||
->get()
|
||||
->getRow('paid_amount');
|
||||
return DB::table('invoices')
|
||||
->where('parent_id', $parentId)
|
||||
->orderByDesc('created_at')
|
||||
->value('total_amount');
|
||||
}
|
||||
|
||||
public function getLatestInvoiceBalance($parentId)
|
||||
public static function getLatestInvoicePaidAmount(int $parentId)
|
||||
{
|
||||
$invoice = $this->where('parent_id', $parentId)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('balance')
|
||||
->get()
|
||||
->getRow();
|
||||
|
||||
return $invoice ? $invoice->balance : null;
|
||||
return DB::table('invoices')
|
||||
->where('parent_id', $parentId)
|
||||
->orderByDesc('created_at')
|
||||
->value('paid_amount');
|
||||
}
|
||||
|
||||
public function getInvoiceEventCharges($invoiceId)
|
||||
public static function getLatestInvoiceBalance(int $parentId)
|
||||
{
|
||||
return $this->db->table('invoice_event')
|
||||
return DB::table('invoices')
|
||||
->where('parent_id', $parentId)
|
||||
->orderByDesc('created_at')
|
||||
->value('balance');
|
||||
}
|
||||
|
||||
public static function getInvoiceEventCharges(int $invoiceId): array
|
||||
{
|
||||
return DB::table('invoice_event')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function getInvoicesByParentId($parentId, $schoolYear = null)
|
||||
public static function getInvoicesByParentId(int $parentId, ?string $schoolYear = null): array
|
||||
{
|
||||
$builder = $this->where('parent_id', $parentId);
|
||||
$q = DB::table('invoices as i')
|
||||
->select('i.*')
|
||||
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
|
||||
->where('i.parent_id', $parentId);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$builder->where('school_year', $schoolYear);
|
||||
$q->where('i.school_year', $schoolYear);
|
||||
}
|
||||
return $builder->orderBy('created_at', 'DESC')->findAll();
|
||||
|
||||
return $q->orderByDesc('i.created_at')->get()->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
|
||||
public function getLatestInvoiceBalanceByYear($parentId, $schoolYear)
|
||||
public static function getLatestInvoiceBalanceByYear(int $parentId, string $schoolYear): ?array
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
$row = DB::table('invoices')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->select('total_amount, paid_amount, balance, updated_at')
|
||||
->get()
|
||||
->getRowArray();
|
||||
->orderByDesc('created_at')
|
||||
->select(['total_amount', 'paid_amount', 'balance', 'updated_at'])
|
||||
->first();
|
||||
|
||||
return $row ? (array) $row : null;
|
||||
}
|
||||
|
||||
public function getDiscountedInvoicesByParent($parentId)
|
||||
public static function getDiscountedInvoicesByParent(int $parentId): array
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
return DB::table('invoices')
|
||||
->where('parent_id', $parentId)
|
||||
->where('has_discount', 1)
|
||||
->findAll();
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* Query helpers
|
||||
* ======================= */
|
||||
|
||||
|
||||
/**
|
||||
* Multi-parent variant (useful when preloading for all parents).
|
||||
* Multi-parent variant (preload invoices for multiple parents).
|
||||
*/
|
||||
public function getInvoicesByUserIds(array $parentIds, string $schoolYear): array
|
||||
public static function getInvoicesByUserIds(array $parentIds, string $schoolYear): array
|
||||
{
|
||||
$parentIds = array_values(array_unique(array_map('intval', array_filter($parentIds))));
|
||||
if (empty($parentIds)) return [];
|
||||
return $this->select('id,parent_id,invoice_number,status,balance,total_amount,issue_date,due_date')
|
||||
->whereIn('parent_id', array_map('intval', $parentIds))
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('issue_date', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->findAll();
|
||||
|
||||
return DB::table('invoices as i')
|
||||
->select(['i.id','i.parent_id','i.invoice_number','i.status','i.balance','i.total_amount','i.issue_date','i.due_date'])
|
||||
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
|
||||
->whereIn('i.parent_id', $parentIds)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->orderByDesc('i.issue_date')
|
||||
->orderByDesc('i.id')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
|
||||
public static function getAllInvoicesByUserIds(array $parentIds, string $schoolYear): array
|
||||
{
|
||||
$parentIds = array_values(array_unique(array_map('intval', array_filter($parentIds))));
|
||||
if (empty($parentIds)) return [];
|
||||
|
||||
return DB::table('invoices as i')
|
||||
->select(['i.id','i.parent_id','i.invoice_number','i.status','i.balance','i.issue_date'])
|
||||
->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount')
|
||||
->whereIn('i.parent_id', $parentIds)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->orderByDesc('i.issue_date')
|
||||
->orderByDesc('i.id')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* Additional charge math
|
||||
* Additional charge math (atomic)
|
||||
* ======================= */
|
||||
|
||||
/**
|
||||
* Atomically add an extra amount to additional_charge (and optionally totals).
|
||||
* Adjust total_amount and balance too (comment out if you don't want that).
|
||||
*/
|
||||
public function applyAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$val = number_format(abs($amount), 2, '.', '');
|
||||
$ok = $this->db->query(
|
||||
"UPDATE {$this->table}
|
||||
SET total_amount = total_amount + {$val},
|
||||
balance = balance + {$val}
|
||||
WHERE id = ?", [$invoiceId]
|
||||
);
|
||||
return $ok && $this->db->affectedRows() > 0;
|
||||
}
|
||||
public static function applyAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$val = number_format(abs($amount), 2, '.', '');
|
||||
|
||||
public function deductAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$val = number_format(abs($amount), 2, '.', '');
|
||||
$ok = $this->db->query(
|
||||
"UPDATE {$this->table}
|
||||
SET total_amount = GREATEST(total_amount - {$val}, 0),
|
||||
balance = GREATEST(balance - {$val}, 0)
|
||||
WHERE id = ?", [$invoiceId]
|
||||
);
|
||||
return $ok && $this->db->affectedRows() > 0;
|
||||
}
|
||||
$affected = DB::update(
|
||||
"UPDATE invoices
|
||||
SET total_amount = total_amount + {$val},
|
||||
balance = balance + {$val}
|
||||
WHERE id = ?",
|
||||
[$invoiceId]
|
||||
);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
public function getAllInvoicesByUserIds(array $parentIds, string $schoolYear): array
|
||||
{
|
||||
if (empty($parentIds)) return [];
|
||||
$parentIds = array_map('intval', $parentIds);
|
||||
return $this->select('id, parent_id, invoice_number, status, balance, issue_date')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->where('school_year', $schoolYear)
|
||||
->orderBy('issue_date', 'DESC')
|
||||
->orderBy('id', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
public static function deductAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$val = number_format(abs($amount), 2, '.', '');
|
||||
|
||||
$affected = DB::update(
|
||||
"UPDATE invoices
|
||||
SET total_amount = GREATEST(total_amount - {$val}, 0),
|
||||
balance = GREATEST(balance - {$val}, 0)
|
||||
WHERE id = ?",
|
||||
[$invoiceId]
|
||||
);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse an extra amount from additional_charge (and totals), never below 0.
|
||||
*/
|
||||
public function reverseAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
public static function reverseAdditionalCharge(int $invoiceId, float $amount): bool
|
||||
{
|
||||
$amount = round($amount, 2);
|
||||
$tb = $this->db->table($this->table);
|
||||
$val = $this->db->escape($amount);
|
||||
return $tb->set('total_amount', "GREATEST(total_amount - $val, 0)", false)
|
||||
->set('balance', "GREATEST(balance - $val, 0)", false)
|
||||
->where('id', $invoiceId)
|
||||
->update();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
$affected = DB::table('invoices')
|
||||
->where('id', $invoiceId)
|
||||
->update([
|
||||
'total_amount' => DB::raw('GREATEST(total_amount - ' . $amount . ', 0)'),
|
||||
'balance' => DB::raw('GREATEST(balance - ' . $amount . ', 0)'),
|
||||
]);
|
||||
|
||||
return $affected >= 0;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use App\Models\BaseModel;
|
||||
class InvoiceEvent extends BaseModel
|
||||
{
|
||||
protected $table = 'invoice_event';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'event_name',
|
||||
@@ -19,7 +22,21 @@ class InvoiceEvent extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
'invoice_id' => 'integer',
|
||||
'amount' => 'decimal:2', // adjust precision if needed
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
public function updatedByUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ use App\Models\BaseModel;
|
||||
|
||||
class InvoiceStudentList extends BaseModel
|
||||
{
|
||||
// Specify the table name
|
||||
protected $table = 'invoice_students_list'; // Primary key for the table
|
||||
protected $primaryKey = 'id';
|
||||
protected $table = 'invoice_students_list';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
// Specify which fields are allowed for mass assignment
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'student_id',
|
||||
@@ -24,15 +24,21 @@ class InvoiceStudentList extends BaseModel
|
||||
'semester',
|
||||
];
|
||||
|
||||
// Enable auto-incrementing primary key
|
||||
protected $useAutoIncrement = true;
|
||||
protected $casts = [
|
||||
'invoice_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'enrolled' => 'boolean',
|
||||
];
|
||||
|
||||
// Specify return type as an array
|
||||
/* Optional relationships */
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
// Automatically set `created_at` and `updated_at` fields
|
||||
public $timestamps = true;
|
||||
|
||||
// Specify the names of the timestamp fields
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
}
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
}
|
||||
+29
-21
@@ -7,7 +7,10 @@ use App\Models\BaseModel;
|
||||
class IpAttempt extends BaseModel
|
||||
{
|
||||
protected $table = 'ip_attempts';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'ip_address',
|
||||
'attempts',
|
||||
@@ -19,34 +22,39 @@ class IpAttempt extends BaseModel
|
||||
'semester',
|
||||
];
|
||||
|
||||
// Automatically handle timestamps
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
// Validation rules
|
||||
protected $validationRules = [
|
||||
'ip_address' => 'required|valid_ip|max_length[45]',
|
||||
'attempts' => 'required|integer',
|
||||
'last_attempt_at' => 'required|valid_date',
|
||||
'blocked_until' => 'permit_empty|valid_date'
|
||||
protected $casts = [
|
||||
'attempts' => 'integer',
|
||||
'last_attempt_at' => 'datetime',
|
||||
'blocked_until' => 'datetime',
|
||||
];
|
||||
|
||||
// Method to get IP attempt data by IP address
|
||||
public function getAttemptByIp($ip)
|
||||
/**
|
||||
* Equivalent of CI getAttemptByIp()
|
||||
*/
|
||||
public static function getAttemptByIp(string $ip): ?self
|
||||
{
|
||||
return $this->where('ip_address', $ip)->first();
|
||||
return static::query()
|
||||
->where('ip_address', $ip)
|
||||
->first();
|
||||
}
|
||||
|
||||
// Method to update the IP attempt data
|
||||
public function updateIpAttempt($ip, $data)
|
||||
/**
|
||||
* Equivalent of CI updateIpAttempt()
|
||||
*/
|
||||
public static function updateIpAttempt(string $ip, array $data): bool
|
||||
{
|
||||
return $this->where('ip_address', $ip)->set($data)->update();
|
||||
return static::query()
|
||||
->where('ip_address', $ip)
|
||||
->update($data) >= 0;
|
||||
}
|
||||
|
||||
// Method to insert new IP attempt data
|
||||
public function insertIpAttempt($data)
|
||||
/**
|
||||
* Equivalent of CI insertIpAttempt()
|
||||
* Returns inserted id.
|
||||
*/
|
||||
public static function insertIpAttempt(array $data): int
|
||||
{
|
||||
return $this->insert($data);
|
||||
$row = static::query()->create($data);
|
||||
return (int) $row->id;
|
||||
}
|
||||
}
|
||||
+33
-14
@@ -3,12 +3,16 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class LateSlipLog extends BaseModel
|
||||
{
|
||||
protected $table = 'late_slip_logs';
|
||||
protected $primaryKey = 'id';
|
||||
public $timestamps = false; // we control printed_at explicitly
|
||||
|
||||
// CI: useTimestamps = false (printed_at is controlled explicitly)
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'school_year',
|
||||
'semester',
|
||||
@@ -22,34 +26,49 @@ class LateSlipLog extends BaseModel
|
||||
'printed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'printed_by' => 'integer',
|
||||
'slip_date' => 'date', // slip_date is Y-m-d
|
||||
'printed_at' => 'datetime',
|
||||
// time_in is often stored as TIME or string; keep as string unless you store as DATETIME
|
||||
'time_in' => 'string',
|
||||
];
|
||||
|
||||
/**
|
||||
* Best-effort insert of a single late slip print log.
|
||||
* Swallows DB errors to avoid blocking printing.
|
||||
*
|
||||
* @param array $data expects keys: school_year, student_name, slip_date (Y-m-d), time_in (H:i:s), grade, reason, admin_name
|
||||
* @param int|null $printedBy user id of the admin performing the print
|
||||
* @return bool success (best-effort)
|
||||
*/
|
||||
public function logSlip(array $data, ?int $printedBy): bool
|
||||
public static function logSlip(array $data, ?int $printedBy): bool
|
||||
{
|
||||
$row = [
|
||||
'school_year' => (string)($data['school_year'] ?? ''),
|
||||
'semester' => (string)($data['semester'] ?? ''),
|
||||
'semester' => (string)($data['semester'] ?? ''),
|
||||
'student_name' => (string)($data['student_name'] ?? ''),
|
||||
'slip_date' => $data['slip_date'] ?? null,
|
||||
'time_in' => $data['time_in'] ?? null,
|
||||
'slip_date' => $data['slip_date'] ?? null, // Y-m-d
|
||||
'time_in' => $data['time_in'] ?? null, // H:i:s
|
||||
'grade' => (string)($data['grade'] ?? ''),
|
||||
'reason' => (string)($data['reason'] ?? ''),
|
||||
'admin_name' => (string)($data['admin_name'] ?? ''),
|
||||
'printed_by' => $printedBy,
|
||||
'printed_at' => utc_now(),
|
||||
'printed_at' => now(), // use UTC if your app timezone is UTC
|
||||
];
|
||||
|
||||
try {
|
||||
return (bool) $this->insert($row);
|
||||
// create() returns model; treat as success if created
|
||||
$created = static::query()->create($row);
|
||||
return !empty($created->id);
|
||||
} catch (QueryException $e) {
|
||||
Log::error('LateSlipLog::logSlip failed: ' . $e->getMessage());
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'LateSlipLog::logSlip failed: ' . $e->getMessage());
|
||||
Log::error('LateSlipLog::logSlip failed: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Optional relationship */
|
||||
public function printer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'printed_by');
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use App\Models\BaseModel;
|
||||
class LoginActivity extends BaseModel
|
||||
{
|
||||
protected $table = 'login_activity';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'email',
|
||||
@@ -20,18 +23,27 @@ class LoginActivity extends BaseModel
|
||||
'semester',
|
||||
'school_year',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
public function getLastActivities($limit = 4)
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'login_time' => 'datetime',
|
||||
'logout_time' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationship */
|
||||
public function user()
|
||||
{
|
||||
return $this->orderBy('login_time', 'DESC')->findAll($limit);
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
/**
|
||||
* Equivalent of CI getLastActivities()
|
||||
*/
|
||||
public static function getLastActivities(int $limit = 4)
|
||||
{
|
||||
return static::query()
|
||||
->orderByDesc('login_time')
|
||||
->limit(max(1, $limit))
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ use App\Models\BaseModel;
|
||||
class ManualPayment extends BaseModel
|
||||
{
|
||||
protected $table = 'manual_payments';
|
||||
|
||||
// CI: useTimestamps = false
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_number',
|
||||
'amount',
|
||||
@@ -18,5 +22,15 @@ class ManualPayment extends BaseModel
|
||||
'school_year',
|
||||
'semester',
|
||||
];
|
||||
public $timestamps = false;
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
'amount' => 'decimal:2', // adjust precision if needed
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationship if invoice_number maps to invoices.invoice_number */
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'invoice_number', 'invoice_number');
|
||||
}
|
||||
}
|
||||
+90
-106
@@ -7,7 +7,10 @@ use App\Models\BaseModel;
|
||||
class Message extends BaseModel
|
||||
{
|
||||
protected $table = 'messages';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// CI: useTimestamps = false (manual date fields)
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'sender_id',
|
||||
'recipient_id',
|
||||
@@ -24,134 +27,115 @@ class Message extends BaseModel
|
||||
'school_year',
|
||||
];
|
||||
|
||||
public $timestamps = false; // Since you're manually handling date fields
|
||||
protected $casts = [
|
||||
'sender_id' => 'integer',
|
||||
'recipient_id' => 'integer',
|
||||
'read_status' => 'boolean',
|
||||
'message_number' => 'integer',
|
||||
|
||||
/**
|
||||
* Get unread messages for a specific recipient.
|
||||
*
|
||||
* @param int $recipientId
|
||||
* @return array
|
||||
*/
|
||||
public function getUnreadMessages($recipientId)
|
||||
'sent_datetime' => 'datetime',
|
||||
'read_datetime' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function sender()
|
||||
{
|
||||
return $this->where('recipient_id', $recipientId)
|
||||
return $this->belongsTo(User::class, 'sender_id');
|
||||
}
|
||||
|
||||
public function recipient()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'recipient_id');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
public static function getUnreadMessages(int $recipientId)
|
||||
{
|
||||
return static::query()
|
||||
->where('recipient_id', $recipientId)
|
||||
->where('read_status', 0)
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
->orderByDesc('sent_datetime')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a message as read and update the read_datetime.
|
||||
*
|
||||
* @param int $messageId
|
||||
* @return bool
|
||||
*/
|
||||
public function markAsRead($messageId)
|
||||
public static function markAsRead(int $messageId): bool
|
||||
{
|
||||
return $this->update($messageId, [
|
||||
'read_status' => 1,
|
||||
'read_datetime' => utc_now() // Set the current datetime when marked as read
|
||||
]);
|
||||
$affected = static::query()
|
||||
->whereKey($messageId)
|
||||
->update([
|
||||
'read_status' => 1,
|
||||
'read_datetime' => now(), // set app timezone (configure to UTC if desired)
|
||||
]);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages by priority.
|
||||
*
|
||||
* @param string $priority
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesByPriority($priority = 'normal')
|
||||
public static function getMessagesByPriority(string $priority = 'normal')
|
||||
{
|
||||
return $this->where('priority', $priority)
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
return static::query()
|
||||
->where('priority', $priority)
|
||||
->orderByDesc('sent_datetime')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages by status.
|
||||
*
|
||||
* @param string $status
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesByStatus($status = 'sent')
|
||||
public static function getMessagesByStatus(string $status = 'sent')
|
||||
{
|
||||
return $this->where('status', $status)
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
return static::query()
|
||||
->where('status', $status)
|
||||
->orderByDesc('sent_datetime')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inbox messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getInboxMessages($userId)
|
||||
public static function getInboxMessages(int $userId)
|
||||
{
|
||||
return $this->where('recipient_id', $userId)
|
||||
return static::query()
|
||||
->where('recipient_id', $userId)
|
||||
->where('status', 'received')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
->orderByDesc('sent_datetime')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sent messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getSentMessages($userId)
|
||||
public static function getSentMessages(int $userId)
|
||||
{
|
||||
return $this->where('sender_id', $userId)
|
||||
->where('status', 'sent')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getDraftMessages($userId)
|
||||
{
|
||||
return $this->where('sender_id', $userId)
|
||||
->where('status', 'draft')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trashed messages for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getTrashedMessages($userId)
|
||||
{
|
||||
return $this->groupStart()
|
||||
return static::query()
|
||||
->where('sender_id', $userId)
|
||||
->orWhere('recipient_id', $userId)
|
||||
->groupEnd()
|
||||
->where('status', 'trashed')
|
||||
->orderBy('sent_datetime', 'DESC')
|
||||
->findAll();
|
||||
->where('status', 'sent')
|
||||
->orderByDesc('sent_datetime')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages for a specific semester and school year.
|
||||
*
|
||||
* @param string $semester
|
||||
* @param string|null $school_year
|
||||
* @return array
|
||||
*/
|
||||
public function getMessagesBySemesterAndYear($semester, $school_year = null)
|
||||
public static function getDraftMessages(int $userId)
|
||||
{
|
||||
$builder = $this->where('semester', $semester);
|
||||
if ($school_year) {
|
||||
$builder->where('school_year', $school_year);
|
||||
return static::query()
|
||||
->where('sender_id', $userId)
|
||||
->where('status', 'draft')
|
||||
->orderByDesc('sent_datetime')
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function getTrashedMessages(int $userId)
|
||||
{
|
||||
return static::query()
|
||||
->where(function ($q) use ($userId) {
|
||||
$q->where('sender_id', $userId)
|
||||
->orWhere('recipient_id', $userId);
|
||||
})
|
||||
->where('status', 'trashed')
|
||||
->orderByDesc('sent_datetime')
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function getMessagesBySemesterAndYear(string $semester, ?string $schoolYear = null)
|
||||
{
|
||||
$q = static::query()->where('semester', $semester);
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$q->where('school_year', $schoolYear);
|
||||
}
|
||||
return $builder->orderBy('sent_datetime', 'DESC')->findAll();
|
||||
|
||||
return $q->orderByDesc('sent_datetime')->get();
|
||||
}
|
||||
}
|
||||
+53
-42
@@ -7,10 +7,13 @@ use App\Models\BaseModel;
|
||||
class MidtermExam extends BaseModel
|
||||
{
|
||||
protected $table = 'midterm_exam';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
|
||||
/**
|
||||
* CI: useTimestamps = false (even though created_at/updated_at columns exist).
|
||||
* Keep it OFF to match behavior (you can still store created_at/updated_at manually).
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
@@ -24,46 +27,54 @@ class MidtermExam extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
|
||||
protected $casts = [];
|
||||
protected $castHandlers = [];
|
||||
// score can be decimal; change to 'integer' if it's whole number
|
||||
'score' => 'decimal:2',
|
||||
|
||||
// Dates
|
||||
public $timestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
// Function to get the final exam score for a specific student, semester, and school year
|
||||
public function getMidtermExamScore($studentId, $schoolYear)
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
// Query the final_score table for the final exam score based on student ID, semester, and school year
|
||||
$result = $this->select('score') // We only need the score
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->first(); // Get the first matching result
|
||||
|
||||
// Return the score, or null if no result is found
|
||||
return $result ? $result['score'] : null;
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getMidtermExamScore()
|
||||
*/
|
||||
public static function getMidtermExamScore(
|
||||
int $studentId,
|
||||
?string $semester,
|
||||
string $schoolYear,
|
||||
?int $classSectionId = null
|
||||
) {
|
||||
$q = static::query()
|
||||
->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
if ($semester !== null && $semester !== '') {
|
||||
$q->where('semester', $semester);
|
||||
}
|
||||
|
||||
if ($classSectionId !== null) {
|
||||
$q->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
$row = $q->first();
|
||||
|
||||
return $row?->score;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MissingScoreOverride extends Model
|
||||
class MissingScoreOverride extends BaseModel
|
||||
{
|
||||
protected $table = 'missing_score_overrides';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'class_section_id',
|
||||
@@ -19,13 +24,129 @@ class MissingScoreOverride extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
/**
|
||||
* TODO: Manual port review required for custom CI query methods from MissingScoreOverride.
|
||||
* - getOverridesMap()
|
||||
* - replaceOverrides()
|
||||
*
|
||||
* This automated conversion preserves schema metadata only.
|
||||
*/
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'item_index' => 'integer',
|
||||
'is_allowed' => 'boolean',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Equivalent of CI getOverridesMap()
|
||||
* Returns: [student_id => [item_index => true]]
|
||||
*/
|
||||
public static function getOverridesMap(int $classSectionId, string $semester, string $schoolYear, string $itemType): array
|
||||
{
|
||||
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '' || $itemType === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = static::query()
|
||||
->select(['student_id', 'item_index'])
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('item_type', $itemType)
|
||||
->where('is_allowed', 1)
|
||||
->get();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $row) {
|
||||
$sid = (int) $row->student_id;
|
||||
$idx = $row->item_index === null ? 0 : (int) $row->item_index;
|
||||
if ($sid > 0) {
|
||||
$map[$sid][$idx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI replaceOverrides()
|
||||
* Deletes matching overrides then inserts checked items.
|
||||
*/
|
||||
public static function replaceOverrides(
|
||||
int $classSectionId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
string $itemType,
|
||||
array $studentIds,
|
||||
?array $itemIndexes,
|
||||
array $checkedItems,
|
||||
?int $updatedBy,
|
||||
bool $nullIndex = false
|
||||
): void {
|
||||
if ($classSectionId <= 0 || $semester === '' || $schoolYear === '' || $itemType === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () use (
|
||||
$classSectionId,
|
||||
$semester,
|
||||
$schoolYear,
|
||||
$itemType,
|
||||
$studentIds,
|
||||
$itemIndexes,
|
||||
$checkedItems,
|
||||
$updatedBy,
|
||||
$nullIndex
|
||||
) {
|
||||
// 1) Delete existing rows in the target scope
|
||||
$q = static::query()
|
||||
->where('class_section_id', $classSectionId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('item_type', $itemType);
|
||||
|
||||
if (!empty($studentIds)) {
|
||||
$q->whereIn('student_id', array_map('intval', $studentIds));
|
||||
}
|
||||
|
||||
if ($itemIndexes !== null) {
|
||||
if (!empty($itemIndexes)) {
|
||||
$q->whereIn('item_index', array_map('intval', $itemIndexes));
|
||||
} else {
|
||||
// CI behavior: if itemIndexes is provided but empty, do nothing.
|
||||
return;
|
||||
}
|
||||
} elseif ($nullIndex) {
|
||||
$q->whereNull('item_index');
|
||||
}
|
||||
|
||||
$q->delete();
|
||||
|
||||
// 2) Insert checked rows
|
||||
if (empty($checkedItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$rows = [];
|
||||
|
||||
foreach ($checkedItems as $item) {
|
||||
$sid = (int) ($item['student_id'] ?? 0);
|
||||
if ($sid <= 0) continue;
|
||||
|
||||
$rows[] = [
|
||||
'student_id' => $sid,
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => $semester,
|
||||
'school_year' => $schoolYear,
|
||||
'item_type' => $itemType,
|
||||
'item_index' => array_key_exists('item_index', $item) ? $item['item_index'] : null,
|
||||
'is_allowed' => 1,
|
||||
'updated_by' => $updatedBy,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($rows)) {
|
||||
DB::table((new static)->getTable())->insert($rows);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+36
-8
@@ -3,13 +3,17 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class NavItem extends BaseModel
|
||||
{
|
||||
protected $table = 'nav_items';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: timestamps + soft deletes
|
||||
use SoftDeletes;
|
||||
|
||||
public $timestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
protected $fillable = [
|
||||
'menu_parent_id',
|
||||
'label',
|
||||
@@ -23,11 +27,35 @@ class NavItem extends BaseModel
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
public function getChildrenOf(int $parentId): array
|
||||
protected $casts = [
|
||||
'menu_parent_id' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
'is_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function parent()
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->where('is_enabled', 1)
|
||||
->orderBy('sort_order', 'ASC')
|
||||
->findAll();
|
||||
return $this->belongsTo(self::class, 'menu_parent_id');
|
||||
}
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'menu_parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getChildrenOf()
|
||||
* NOTE: CI code uses where('parent_id', ...) but allowedFields has menu_parent_id.
|
||||
* This Laravel version uses menu_parent_id (likely correct).
|
||||
*/
|
||||
public static function getChildrenOf(int $parentId): array
|
||||
{
|
||||
return static::query()
|
||||
->where('menu_parent_id', $parentId)
|
||||
->where('is_enabled', 1)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
+54
-34
@@ -3,11 +3,17 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Notification extends BaseModel
|
||||
{
|
||||
protected $table = 'notifications';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: soft deletes + timestamps
|
||||
use SoftDeletes;
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'message',
|
||||
@@ -26,64 +32,78 @@ class Notification extends BaseModel
|
||||
'school_year',
|
||||
'semester',
|
||||
];
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
protected $casts = [
|
||||
'scheduled_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
// If delivery_channels is JSON in DB:
|
||||
'delivery_channels' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get active (non-deleted, non-expired) notifications
|
||||
* Equivalent of CI getActiveNotifications()
|
||||
* Active = scheduled_at <= now AND (expires_at is null OR expires_at > now)
|
||||
*/
|
||||
public function getActiveNotifications($targetGroup = null)
|
||||
public static function getActiveNotifications(?string $targetGroup = null): array
|
||||
{
|
||||
$builder = $this->where('scheduled_at <= NOW()')
|
||||
->where('(expires_at IS NULL OR expires_at > NOW())');
|
||||
$q = static::query()
|
||||
->where('scheduled_at', '<=', now())
|
||||
->where(function ($w) {
|
||||
$w->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
|
||||
if (!empty($targetGroup)) {
|
||||
$builder->where('target_group', $targetGroup);
|
||||
$q->where('target_group', $targetGroup);
|
||||
}
|
||||
|
||||
return $builder->orderBy('priority', 'DESC')
|
||||
->orderBy('scheduled_at', 'DESC')
|
||||
->findAll();
|
||||
return $q->orderByDesc('priority')
|
||||
->orderByDesc('scheduled_at')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get soft-deleted (archived) notifications
|
||||
* Equivalent of CI getDeletedNotifications()
|
||||
*/
|
||||
public function getDeletedNotifications()
|
||||
public static function getDeletedNotifications(): array
|
||||
{
|
||||
return $this->onlyDeleted()
|
||||
->orderBy('deleted_at', 'DESC')
|
||||
->findAll();
|
||||
return static::onlyTrashed()
|
||||
->orderByDesc('deleted_at')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications including soft-deleted
|
||||
* Equivalent of CI getAllNotificationsWithDeleted()
|
||||
*/
|
||||
public function getAllNotificationsWithDeleted()
|
||||
public static function getAllNotificationsWithDeleted(): array
|
||||
{
|
||||
return $this->withDeleted()
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
return static::withTrashed()
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a soft-deleted notification by ID
|
||||
* Equivalent of CI restoreNotification()
|
||||
*/
|
||||
public function restoreNotification($id)
|
||||
public static function restoreNotification(int $id): bool
|
||||
{
|
||||
return $this->update($id, ['deleted_at' => null]);
|
||||
$row = static::withTrashed()->find($id);
|
||||
if (!$row) return false;
|
||||
return (bool) $row->restore();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cleanup expired notifications (optional for cron job use)
|
||||
* Equivalent of CI deleteExpiredNotifications()
|
||||
* Soft-deletes rows with expires_at < now (and expires_at not null)
|
||||
*/
|
||||
public function deleteExpiredNotifications()
|
||||
public static function deleteExpiredNotifications(): int
|
||||
{
|
||||
return $this->where('expires_at IS NOT NULL')
|
||||
->where('expires_at < NOW()')
|
||||
->delete();
|
||||
return static::query()
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<', now())
|
||||
->delete(); // soft delete
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ParentAttendanceReport extends BaseModel
|
||||
{
|
||||
protected $table = 'parent_attendance_reports';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
// ✅ CI: useTimestamps = true
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'student_id',
|
||||
@@ -28,42 +28,63 @@ class ParentAttendanceReport extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected $validationRules = [
|
||||
'parent_id' => 'required|is_natural_no_zero',
|
||||
'student_id' => 'required|is_natural_no_zero',
|
||||
'report_date' => 'required|valid_date[Y-m-d]',
|
||||
'type' => 'required|in_list[absent,late,early_dismissal]',
|
||||
'arrival_time' => 'permit_empty|regex_match[/^\d{2}:\d{2}(:\d{2})?$/]',
|
||||
'dismiss_time' => 'permit_empty|regex_match[/^\d{2}:\d{2}(:\d{2})?$/]',
|
||||
'semester' => 'permit_empty|string',
|
||||
'school_year' => 'permit_empty|string',
|
||||
'status' => 'permit_empty|in_list[new,seen,processed]',
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'class_section_id'=> 'integer',
|
||||
'report_date' => 'date',
|
||||
];
|
||||
|
||||
public function listForDateRange(?string $startDate = null, ?string $endDate = null, ?string $schoolYear = null, ?string $semester = null): array
|
||||
/* Optional relationships */
|
||||
public function parent()
|
||||
{
|
||||
$b = $this->builder();
|
||||
if ($startDate) {
|
||||
$b->where('report_date >=', $startDate);
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
// classSection table uses class_section_id as “business key”; your join uses that.
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id', 'class_section_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel equivalent of CI listForDateRange()
|
||||
* Returns array rows with:
|
||||
* parent_attendance_reports.* + students firstname/lastname + classSection class_section_name
|
||||
*/
|
||||
public static function listForDateRange(
|
||||
?string $startDate = null,
|
||||
?string $endDate = null,
|
||||
?string $schoolYear = null,
|
||||
?string $semester = null
|
||||
): array {
|
||||
$q = DB::table('parent_attendance_reports as par');
|
||||
|
||||
if (!empty($startDate)) {
|
||||
$q->where('par.report_date', '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$b->where('report_date <=', $endDate);
|
||||
if (!empty($endDate)) {
|
||||
$q->where('par.report_date', '<=', $endDate);
|
||||
}
|
||||
if (is_string($schoolYear) && $schoolYear !== '') {
|
||||
$b->where('school_year', $schoolYear);
|
||||
$q->where('par.school_year', $schoolYear);
|
||||
}
|
||||
if (is_string($semester) && $semester !== '') {
|
||||
$b->where('semester', $semester);
|
||||
$q->where('par.semester', $semester);
|
||||
}
|
||||
|
||||
// Join student + class section labels
|
||||
$b->select('parent_attendance_reports.*, s.firstname, s.lastname, cs.class_section_name')
|
||||
->join('students s', 's.id = parent_attendance_reports.student_id', 'left')
|
||||
->join('classSection cs', 'cs.class_section_id = parent_attendance_reports.class_section_id', 'left')
|
||||
->orderBy('report_date', 'ASC')
|
||||
->orderBy('class_section_id', 'ASC')
|
||||
->orderBy('student_id', 'ASC');
|
||||
$q->leftJoin('students as s', 's.id', '=', 'par.student_id')
|
||||
->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'par.class_section_id')
|
||||
->select('par.*', 's.firstname', 's.lastname', 'cs.class_section_name')
|
||||
->orderBy('par.report_date', 'asc')
|
||||
->orderBy('par.class_section_id', 'asc')
|
||||
->orderBy('par.student_id', 'asc');
|
||||
|
||||
return $b->get()->getResultArray();
|
||||
return $q->get()->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class ParentMeetingSchedule extends Model
|
||||
class ParentMeetingSchedule extends BaseModel
|
||||
{
|
||||
protected $table = 'parent_meeting_schedules';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'parent_user_id',
|
||||
@@ -23,5 +27,31 @@ class ParentMeetingSchedule extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'parent_user_id' => 'integer',
|
||||
'created_by' => 'integer',
|
||||
|
||||
// if these are DATE/TIME columns:
|
||||
'date' => 'date',
|
||||
// time is typically stored as TIME or string; keep as string unless DATETIME
|
||||
'time' => 'string',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'parent_user_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use App\Models\BaseModel;
|
||||
class ParentModel extends BaseModel
|
||||
{
|
||||
protected $table = 'parents';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// CI model didn't specify timestamps
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'secondparent_firstname',
|
||||
'secondparent_lastname',
|
||||
@@ -21,5 +24,14 @@ class ParentModel extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
'firstparent_id' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationship */
|
||||
public function firstParentUser()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'firstparent_id');
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@ use App\Models\BaseModel;
|
||||
class ParentNotification extends BaseModel
|
||||
{
|
||||
protected $table = 'parent_notifications';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'code',
|
||||
@@ -26,15 +26,40 @@ class ParentNotification extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function hasSent(int $studentId, string $code, string $incidentYmd, string $channel='email', ?string $to=null): bool
|
||||
{
|
||||
$qb = $this->where('student_id', $studentId)
|
||||
->where('code', $code)
|
||||
->where('incident_date', $incidentYmd)
|
||||
->where('channel', $channel);
|
||||
if ($to) $qb->where('to_address', $to);
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
// incident_date is stored as Y-m-d in CI usage
|
||||
'incident_date' => 'date',
|
||||
];
|
||||
|
||||
$row = $qb->orderBy('id','DESC')->first();
|
||||
return $row && ($row['status'] ?? '') === 'sent';
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI hasSent()
|
||||
*/
|
||||
public static function hasSent(
|
||||
int $studentId,
|
||||
string $code,
|
||||
string $incidentYmd,
|
||||
string $channel = 'email',
|
||||
?string $to = null
|
||||
): bool {
|
||||
$q = static::query()
|
||||
->where('student_id', $studentId)
|
||||
->where('code', $code)
|
||||
->whereDate('incident_date', $incidentYmd)
|
||||
->where('channel', $channel);
|
||||
|
||||
if (!empty($to)) {
|
||||
$q->where('to_address', $to);
|
||||
}
|
||||
|
||||
$row = $q->orderByDesc('id')->first();
|
||||
|
||||
return $row && (($row->status ?? '') === 'sent');
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,13 @@ use App\Models\BaseModel;
|
||||
class Participation extends BaseModel
|
||||
{
|
||||
protected $table = 'participation';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
|
||||
/**
|
||||
* CI: useTimestamps = false (even though created_at/updated_at columns exist).
|
||||
* Keep OFF to match behavior (you can still set created_at/updated_at manually).
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
@@ -24,45 +27,60 @@ class Participation extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
|
||||
protected $casts = [];
|
||||
protected $castHandlers = [];
|
||||
// score is numeric; change to 'integer' if it is whole number
|
||||
'score' => 'decimal:2',
|
||||
|
||||
// Dates
|
||||
public $timestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
// Function to get the final exam score for a specific student, semester, and school year
|
||||
public function getParticipationScore(int $studentId, string $semester, string $schoolYear): ?float
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
$result = $this->select('score')
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function classSection()
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI getParticipationScore()
|
||||
* Returns null if missing/blank/non-numeric.
|
||||
*/
|
||||
public static function getParticipationScore(
|
||||
int $studentId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $classSectionId = null
|
||||
): ?float {
|
||||
$q = static::query()
|
||||
->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->first();
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
return $result ? floatval($result['score']) : null;
|
||||
if ($classSectionId !== null) {
|
||||
$q->where('class_section_id', $classSectionId);
|
||||
}
|
||||
|
||||
$row = $q->first();
|
||||
if (!$row) return null;
|
||||
|
||||
$score = $row->score;
|
||||
|
||||
if ($score === null) return null;
|
||||
if (is_string($score) && trim($score) === '') return null;
|
||||
if ($score === '') return null;
|
||||
if (!is_numeric($score)) return null;
|
||||
|
||||
return (float) $score;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
// app/Models/PasswordReset.php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
@@ -8,14 +7,42 @@ use App\Models\BaseModel;
|
||||
class PasswordReset extends BaseModel
|
||||
{
|
||||
protected $table = 'password_resets';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* In CI you used:
|
||||
* - createdField = created_at
|
||||
* - updatedField = expires_at (non-standard)
|
||||
*
|
||||
* In Laravel, we can:
|
||||
* - enable timestamps
|
||||
* - map UPDATED_AT to expires_at
|
||||
*
|
||||
* This will auto-set expires_at whenever you call save()/update().
|
||||
* (Only do this if that’s truly what you want.)
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'expires_at';
|
||||
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'token',
|
||||
'created_at',
|
||||
'expires_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'expires_at';
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Optional: if your password_resets table does NOT have an auto-increment id
|
||||
* (Laravel default table usually uses email as primary and no id),
|
||||
* uncomment below and set the correct primary key settings:
|
||||
*/
|
||||
// protected $primaryKey = 'id';
|
||||
// public $incrementing = true;
|
||||
// protected $keyType = 'int';
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
<?php
|
||||
|
||||
// app/Models/PasswordResetRequest.php
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class PasswordResetRequest extends BaseModel
|
||||
{
|
||||
protected $table = 'password_reset_requests';
|
||||
|
||||
// CI: timestamps off
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'ip_address',
|
||||
'requested_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
'requested_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
@@ -4,11 +4,17 @@ namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
|
||||
|
||||
class PayPalPayment extends BaseModel
|
||||
{
|
||||
protected $table = 'paypal_payments';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* CI: created_at is managed, updated_at is NOT used.
|
||||
* In Laravel: enable timestamps but disable UPDATED_AT.
|
||||
*/
|
||||
public $timestamps = false;
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'webhook_id',
|
||||
'parent_school_id',
|
||||
@@ -29,9 +35,12 @@ class PayPalPayment extends BaseModel
|
||||
'created_at',
|
||||
];
|
||||
|
||||
// Enable timestamps
|
||||
public $timestamps = false;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = ''; // not used; leave empty if not needed
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
'parent_school_id' => 'integer',
|
||||
'amount' => 'decimal:2',
|
||||
'paypal_fee' => 'decimal:2',
|
||||
'net_amount' => 'decimal:2',
|
||||
'synced' => 'boolean',
|
||||
'sync_attempts' => 'integer',
|
||||
];
|
||||
}
|
||||
+107
-143
@@ -3,11 +3,18 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Payment extends BaseModel
|
||||
{
|
||||
protected $table = 'payments';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* CI: useTimestamps = false because DB handles created_at/updated_at (defaults/triggers).
|
||||
* Keep OFF in Laravel too.
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'invoice_id',
|
||||
@@ -27,184 +34,141 @@ class Payment extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
// ❗ Your DB handles created_at / updated_at with default CURRENT_TIMESTAMP
|
||||
// Remove CI4 auto timestamp management unless you modify your DB schema
|
||||
|
||||
protected $validationRules = [
|
||||
'parent_id' => 'required|integer',
|
||||
'invoice_id' => 'required|integer',
|
||||
'total_amount' => 'required|decimal',
|
||||
'paid_amount' => 'required|decimal',
|
||||
'balance' => 'required|decimal',
|
||||
'number_of_installments' => 'required|integer',
|
||||
'payment_method' => 'required|max_length[50]', // ✅ Removed 'string'
|
||||
'payment_date' => 'required|valid_date[Y-m-d H:i:s]',
|
||||
'status' => 'required|max_length[50]', // ✅ Removed 'string'
|
||||
'check_number' => 'permit_empty|max_length[100]',
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'number_of_installments' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
|
||||
'total_amount' => 'decimal:2',
|
||||
'paid_amount' => 'decimal:2',
|
||||
'balance' => 'decimal:2',
|
||||
|
||||
'payment_date' => 'datetime',
|
||||
];
|
||||
|
||||
protected $validationMessages = [
|
||||
'parent_id' => [
|
||||
'required' => 'Parent ID is required.',
|
||||
'integer' => 'Parent ID must be an integer.',
|
||||
],
|
||||
'invoice_id' => [
|
||||
'required' => 'Invoice ID is required.',
|
||||
'integer' => 'Invoice ID must be an integer.',
|
||||
],
|
||||
'total_amount' => [
|
||||
'required' => 'Total amount is required.',
|
||||
'decimal' => 'Total amount must be a valid decimal.',
|
||||
],
|
||||
'paid_amount' => [
|
||||
'required' => 'Paid amount is required.',
|
||||
'decimal' => 'Paid amount must be a valid decimal.',
|
||||
],
|
||||
'balance' => [
|
||||
'required' => 'Balance is required.',
|
||||
'decimal' => 'Balance must be a valid decimal.',
|
||||
],
|
||||
'payment_date' => [
|
||||
'required' => 'Payment date is required.',
|
||||
'valid_date' => 'Payment date must be a valid date (Y-m-d H:i:s).',
|
||||
],
|
||||
'payment_method' => [
|
||||
'required' => 'Payment method is required.',
|
||||
'max_length' => 'Payment method must not exceed 50 characters.',
|
||||
],
|
||||
'status' => [
|
||||
'required' => 'Status is required.',
|
||||
'max_length' => 'Status must not exceed 50 characters.',
|
||||
],
|
||||
'number_of_installments' => [
|
||||
'required' => 'Number of installments is required.',
|
||||
'integer' => 'Number of installments must be an integer.',
|
||||
],
|
||||
'check_number' => [
|
||||
'max_length' => 'Check number must not exceed 100 characters.',
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Get payments by parent ID.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @return array
|
||||
*/
|
||||
public function getPaymentsByParentId(int $parentId): array
|
||||
/* Optional relationships */
|
||||
public function invoice()
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
->orderBy('payment_date', 'DESC')
|
||||
->findAll();
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total paid amount for a specific parent.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @return float
|
||||
*/
|
||||
public function getTotalPaidByParentId(int $parentId, string $schoolYear): float
|
||||
public function parent()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
$result = $db->table('payments')
|
||||
->selectSum('paid_amount', 'total_paid')
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
public static function getPaymentsByParentId(int $parentId): array
|
||||
{
|
||||
return static::query()
|
||||
->where('parent_id', $parentId)
|
||||
->orderByDesc('payment_date')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function getTotalPaidByParentId(int $parentId, string $schoolYear): float
|
||||
{
|
||||
$total = DB::table('payments')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->sum('paid_amount');
|
||||
|
||||
return (float) $total;
|
||||
}
|
||||
|
||||
public static function updateBalance(int $paymentId, float $amountPaid): bool
|
||||
{
|
||||
/** @var self|null $payment */
|
||||
$payment = static::query()->find($paymentId);
|
||||
if (!$payment) return false;
|
||||
|
||||
$newBalance = (float)$payment->balance - (float)$amountPaid;
|
||||
|
||||
// keep precision stable
|
||||
$paidAmount = (float)$payment->paid_amount + (float)$amountPaid;
|
||||
|
||||
$affected = static::query()
|
||||
->whereKey($paymentId)
|
||||
->update([
|
||||
'paid_amount' => $paidAmount,
|
||||
'balance' => $newBalance,
|
||||
]);
|
||||
|
||||
return $affected > 0;
|
||||
}
|
||||
|
||||
public static function getPaymentsByYear(string $schoolYear): array
|
||||
{
|
||||
return static::query()
|
||||
->where('school_year', $schoolYear)
|
||||
->orderByDesc('payment_date')
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return $result && isset($result['total_paid']) ? (float) $result['total_paid'] : 0.00;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update balance for a specific payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @param float $amountPaid
|
||||
* @return bool
|
||||
*/
|
||||
public function updateBalance(int $paymentId, float $amountPaid): bool
|
||||
{
|
||||
$payment = $this->find($paymentId);
|
||||
if (!$payment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$newBalance = $payment['balance'] - $amountPaid;
|
||||
|
||||
return $this->update($paymentId, [
|
||||
'paid_amount' => $payment['paid_amount'] + $amountPaid,
|
||||
'balance' => $newBalance
|
||||
]);
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payments by school year.
|
||||
*
|
||||
* @param string $schoolYear
|
||||
* @return array
|
||||
* Generate a new transaction ID: YYYY-000001
|
||||
*/
|
||||
public function getPaymentsByYear(string $schoolYear): array
|
||||
{
|
||||
return $this->where('school_year', $schoolYear)
|
||||
->orderBy('payment_date', 'DESC')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new transaction ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateNewTransactionId(): string
|
||||
public static function generateNewTransactionId(): string
|
||||
{
|
||||
$currentYear = date('Y');
|
||||
$prefix = $currentYear . '-';
|
||||
|
||||
$latest = $this->like('transaction_id', $prefix, 'after')
|
||||
->orderBy('transaction_id', 'DESC')
|
||||
$latest = static::query()
|
||||
->where('transaction_id', 'like', $prefix . '%')
|
||||
->orderByDesc('transaction_id')
|
||||
->first();
|
||||
|
||||
if ($latest && isset($latest['transaction_id'])) {
|
||||
$lastNumber = (int) str_replace($prefix, '', $latest['transaction_id']);
|
||||
if ($latest && !empty($latest->transaction_id)) {
|
||||
$lastNumber = (int) str_replace($prefix, '', (string)$latest->transaction_id);
|
||||
$newNumber = $lastNumber + 1;
|
||||
} else {
|
||||
$newNumber = 1;
|
||||
}
|
||||
|
||||
return $prefix . str_pad($newNumber, 6, '0', STR_PAD_LEFT);
|
||||
return $prefix . str_pad((string)$newNumber, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public function getPaymentsByInvoice($invoiceIds): array
|
||||
/**
|
||||
* Get latest payment (date/amount/status) per invoice for given invoice IDs.
|
||||
* Matches your CI logic: statuses in ['Full','Paid','Partially Paid'].
|
||||
*/
|
||||
public static function getPaymentsByInvoice($invoiceIds): array
|
||||
{
|
||||
// Normalize input
|
||||
if (!is_array($invoiceIds)) {
|
||||
$invoiceIds = [$invoiceIds];
|
||||
}
|
||||
if (empty($invoiceIds)) {
|
||||
return [];
|
||||
}
|
||||
$invoiceIds = array_values(array_unique(array_map('intval', array_filter($invoiceIds))));
|
||||
if (empty($invoiceIds)) return [];
|
||||
|
||||
// Subquery: latest payment_date per invoice (Full/Partial only)
|
||||
$latestSub = $this->db->table('payments')
|
||||
->select('invoice_id, MAX(payment_date) AS max_date')
|
||||
->whereIn('status', ['Full', 'Partially Paid'])
|
||||
->groupBy('invoice_id')
|
||||
->getCompiledSelect();
|
||||
$latestSub = DB::table('payments')
|
||||
->selectRaw('invoice_id, MAX(payment_date) AS max_date')
|
||||
->whereIn('status', ['Full', 'Paid', 'Partially Paid'])
|
||||
->groupBy('invoice_id');
|
||||
|
||||
// Main query: join payments to the latest per invoice
|
||||
$rows = $this->db->table('payments p')
|
||||
->select('p.invoice_id, p.paid_amount AS last_paid_amount, p.payment_date AS last_payment_date, p.status AS last_payment_status')
|
||||
->join("($latestSub) latest", 'latest.invoice_id = p.invoice_id AND latest.max_date = p.payment_date', 'inner', false) // <-- escape=false
|
||||
$rows = DB::table('payments as p')
|
||||
->joinSub($latestSub, 'latest', function ($join) {
|
||||
$join->on('latest.invoice_id', '=', 'p.invoice_id')
|
||||
->on('latest.max_date', '=', 'p.payment_date');
|
||||
})
|
||||
->whereIn('p.invoice_id', $invoiceIds)
|
||||
->whereIn('p.status', ['Full', 'Partially Paid'])
|
||||
->get()
|
||||
->getResultArray();
|
||||
->whereIn('p.status', ['Full', 'Paid', 'Partially Paid'])
|
||||
->select([
|
||||
'p.invoice_id',
|
||||
DB::raw('p.paid_amount AS last_paid_amount'),
|
||||
DB::raw('p.payment_date AS last_payment_date'),
|
||||
DB::raw('p.status AS last_payment_status'),
|
||||
])
|
||||
->get();
|
||||
|
||||
return $rows;
|
||||
return $rows->map(fn ($r) => (array) $r)->all();
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-10
@@ -6,8 +6,11 @@ use App\Models\BaseModel;
|
||||
|
||||
class PaymentError extends BaseModel
|
||||
{
|
||||
protected $table = 'payment_error'; // The DB table
|
||||
protected $primaryKey = 'id'; // Primary key
|
||||
protected $table = 'payment_error';
|
||||
|
||||
// CI: useTimestamps = false (logged_at managed manually)
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'payment_id',
|
||||
'invoice_id',
|
||||
@@ -22,12 +25,28 @@ class PaymentError extends BaseModel
|
||||
'semester',
|
||||
];
|
||||
|
||||
public $timestamps = false; // We'll manage logged_at manually
|
||||
protected $validationRules = [
|
||||
'wrong_paid_amount' => 'required|decimal',
|
||||
'payment_id' => 'required|integer',
|
||||
'invoice_id' => 'required|integer',
|
||||
'parent_id' => 'required|integer'
|
||||
];
|
||||
protected $casts = [
|
||||
'payment_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'parent_id' => 'integer',
|
||||
'wrong_paid_amount' => 'decimal:2', // adjust precision if needed
|
||||
'logged_by' => 'integer',
|
||||
'logged_at' => 'datetime',
|
||||
];
|
||||
|
||||
}
|
||||
/* Optional relationships */
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function logger()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'logged_by');
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ use App\Models\BaseModel;
|
||||
class PaymentNotificationLog extends BaseModel
|
||||
{
|
||||
protected $table = 'payment_notification_logs';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useSoftDeletes = false;
|
||||
|
||||
// CI: useTimestamps = false (created_at / sent_at are managed manually)
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'invoice_id',
|
||||
@@ -27,30 +29,60 @@ class PaymentNotificationLog extends BaseModel
|
||||
'created_at',
|
||||
'sent_at',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
public function existsForPeriod(int $parentId, int $year, int $month, string $type): bool
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'period_year' => 'integer',
|
||||
'period_month' => 'integer',
|
||||
'head_fa_notified' => 'boolean',
|
||||
'balance_snapshot' => 'decimal:2', // adjust precision if needed
|
||||
'created_at' => 'datetime',
|
||||
'sent_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function parent()
|
||||
{
|
||||
return $this->where('parent_id', $parentId)
|
||||
return $this->belongsTo(User::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent of CI existsForPeriod()
|
||||
*/
|
||||
public static function existsForPeriod(int $parentId, int $year, int $month, string $type): bool
|
||||
{
|
||||
return static::query()
|
||||
->where('parent_id', $parentId)
|
||||
->where('period_year', $year)
|
||||
->where('period_month', $month)
|
||||
->where('type', $type)
|
||||
->first() !== null;
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function listLogs(?string $from = null, ?string $to = null, ?string $type = null): array
|
||||
/**
|
||||
* Equivalent of CI listLogs()
|
||||
* $from/$to should be DATETIME strings.
|
||||
*/
|
||||
public static function listLogs(?string $from = null, ?string $to = null, ?string $type = null): array
|
||||
{
|
||||
$builder = $this->orderBy('sent_at', 'DESC');
|
||||
if ($from) {
|
||||
$builder->where('sent_at >=', $from);
|
||||
}
|
||||
if ($to) {
|
||||
$builder->where('sent_at <=', $to);
|
||||
}
|
||||
if ($type) {
|
||||
$builder->where('type', $type);
|
||||
}
|
||||
return $builder->findAll();
|
||||
}
|
||||
}
|
||||
$q = static::query()->orderByDesc('sent_at');
|
||||
|
||||
if (!empty($from)) {
|
||||
$q->where('sent_at', '>=', $from);
|
||||
}
|
||||
if (!empty($to)) {
|
||||
$q->where('sent_at', '<=', $to);
|
||||
}
|
||||
if (!empty($type)) {
|
||||
$q->where('type', $type);
|
||||
}
|
||||
|
||||
return $q->get()->toArray();
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PaymentTransaction extends BaseModel
|
||||
{
|
||||
protected $table = 'payment_transactions'; // Table name
|
||||
protected $primaryKey = 'id'; // Primary key
|
||||
protected $table = 'payment_transactions';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = false;
|
||||
|
||||
// Allowed fields for mass assignment
|
||||
protected $fillable = [
|
||||
'transaction_id',
|
||||
'payment_id',
|
||||
@@ -22,123 +24,71 @@ class PaymentTransaction extends BaseModel
|
||||
'semester',
|
||||
];
|
||||
|
||||
// Set automatic timestamps
|
||||
public $timestamps = false;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
// Validation rules
|
||||
protected $validationRules = [
|
||||
'transaction_id' => 'required|is_unique[payment_transactions.transaction_id]',
|
||||
'payment_id' => 'required|integer', // Reference to payment table
|
||||
'amount' => 'required|decimal',
|
||||
'payment_method' => 'required|string|max_length[50]',
|
||||
'payment_status' => 'required|string|max_length[50]',
|
||||
'payment_reference' => 'permit_empty|string|max_length[255]',
|
||||
'is_full_payment' => 'required|in_list[0,1]', // 0 for installment, 1 for full payment
|
||||
protected $casts = [
|
||||
'payment_id' => 'integer',
|
||||
'amount' => 'decimal:2', // adjust precision if needed
|
||||
'transaction_fee' => 'decimal:2', // adjust precision if needed
|
||||
'transaction_date' => 'datetime',
|
||||
'is_full_payment' => 'boolean',
|
||||
];
|
||||
|
||||
// Custom error messages
|
||||
protected $validationMessages = [
|
||||
'transaction_id' => [
|
||||
'required' => 'The transaction ID is required.',
|
||||
'is_unique' => 'The transaction ID must be unique.',
|
||||
],
|
||||
'payment_id' => [
|
||||
'required' => 'The payment ID is required.',
|
||||
'integer' => 'The payment ID must be an integer.',
|
||||
],
|
||||
'amount' => [
|
||||
'required' => 'The payment amount is required.',
|
||||
'decimal' => 'The payment amount must be a valid decimal number.',
|
||||
],
|
||||
'payment_method' => [
|
||||
'required' => 'The payment method is required.',
|
||||
'max_length' => 'The payment method must not exceed 50 characters.',
|
||||
],
|
||||
'payment_status' => [
|
||||
'required' => 'The payment status is required.',
|
||||
'max_length' => 'The payment status must not exceed 50 characters.',
|
||||
],
|
||||
'payment_reference' => [
|
||||
'max_length' => 'The payment reference must not exceed 255 characters.',
|
||||
],
|
||||
'is_full_payment' => [
|
||||
'required' => 'The full payment flag is required.',
|
||||
'in_list' => 'The full payment flag must be either 0 (installment) or 1 (full payment).',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all transactions for a specific payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @return array
|
||||
*/
|
||||
public function getTransactionsByPaymentId($paymentId)
|
||||
/* Optional relationship */
|
||||
public function payment()
|
||||
{
|
||||
return $this->where('payment_id', $paymentId)
|
||||
->orderBy('transaction_date', 'DESC')
|
||||
->findAll();
|
||||
return $this->belongsTo(Payment::class, 'payment_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single transaction by its transaction ID.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTransactionById($transactionId)
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
public static function getTransactionsByPaymentId(int $paymentId): array
|
||||
{
|
||||
return $this->where('transaction_id', $transactionId)->first();
|
||||
return static::query()
|
||||
->where('payment_id', $paymentId)
|
||||
->orderByDesc('transaction_date')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update payment status by transaction ID.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @param string $status
|
||||
* @return bool
|
||||
*/
|
||||
public function updateTransactionStatus($transactionId, $status)
|
||||
public static function getTransactionById(string $transactionId): ?self
|
||||
{
|
||||
return $this->update($transactionId, ['payment_status' => $status]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total amount paid for a specific payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @return float
|
||||
*/
|
||||
public function getTotalPaidByPaymentId($paymentId)
|
||||
{
|
||||
$this->selectSum('amount');
|
||||
return $this->where('payment_id', $paymentId)->get()->getRow()->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update transaction fee for a specific transaction.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @param float $transactionFee
|
||||
* @return bool
|
||||
*/
|
||||
public function updateTransactionFee($transactionId, $transactionFee)
|
||||
{
|
||||
return $this->update($transactionId, ['transaction_fee' => $transactionFee]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest payment transaction for a payment.
|
||||
*
|
||||
* @param int $paymentId
|
||||
* @return array|null
|
||||
*/
|
||||
public function getLatestTransactionByPaymentId($paymentId)
|
||||
{
|
||||
return $this->where('payment_id', $paymentId)
|
||||
->orderBy('transaction_date', 'DESC')
|
||||
return static::query()
|
||||
->where('transaction_id', $transactionId)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update payment_status by transaction_id (not PK).
|
||||
*/
|
||||
public static function updateTransactionStatus(string $transactionId, string $status): bool
|
||||
{
|
||||
return static::query()
|
||||
->where('transaction_id', $transactionId)
|
||||
->update(['payment_status' => $status]) >= 0;
|
||||
}
|
||||
|
||||
public static function getTotalPaidByPaymentId(int $paymentId): float
|
||||
{
|
||||
$sum = static::query()
|
||||
->where('payment_id', $paymentId)
|
||||
->sum('amount');
|
||||
|
||||
return (float) $sum;
|
||||
}
|
||||
|
||||
public static function updateTransactionFee(string $transactionId, float $transactionFee): bool
|
||||
{
|
||||
return static::query()
|
||||
->where('transaction_id', $transactionId)
|
||||
->update(['transaction_fee' => $transactionFee]) >= 0;
|
||||
}
|
||||
|
||||
public static function getLatestTransactionByPaymentId(int $paymentId): ?self
|
||||
{
|
||||
return static::query()
|
||||
->where('payment_id', $paymentId)
|
||||
->orderByDesc('transaction_date')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use App\Models\BaseModel;
|
||||
class PaypalTransaction extends BaseModel
|
||||
{
|
||||
protected $table = 'paypal_transactions';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'transaction_id',
|
||||
'payment_id',
|
||||
@@ -26,5 +29,18 @@ class PaypalTransaction extends BaseModel
|
||||
'school_year',
|
||||
'semester',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'payment_id' => 'integer',
|
||||
'amount' => 'decimal:2', // adjust precision if needed
|
||||
'transaction_fee' => 'decimal:2', // adjust precision if needed
|
||||
// raw_data is often JSON; if it's JSON in DB, cast to array
|
||||
'raw_data' => 'array',
|
||||
];
|
||||
|
||||
/* Optional relationship */
|
||||
public function payment()
|
||||
{
|
||||
return $this->belongsTo(Payment::class, 'payment_id');
|
||||
}
|
||||
}
|
||||
+35
-41
@@ -3,67 +3,61 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Permission extends BaseModel
|
||||
{
|
||||
protected $table = 'permissions'; // Define the table associated with this model
|
||||
protected $primaryKey = 'id'; // Define the primary key of the table
|
||||
protected $table = 'permissions';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]; // Define the fields that can be set using insert/update methods
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch all permissions from the database
|
||||
*
|
||||
* @return array List of permissions
|
||||
* @throws \Exception If there is an error during the fetch
|
||||
/* =========================
|
||||
* CI method equivalents
|
||||
* ========================= */
|
||||
|
||||
/**
|
||||
* Fetch all permissions from the database.
|
||||
*/
|
||||
public function getPermissions()
|
||||
public static function getPermissions()
|
||||
{
|
||||
try {
|
||||
// Query to get all permissions from the 'permissions' table
|
||||
return $this->findAll();
|
||||
} catch (\Exception $e) {
|
||||
// Log the error message if the query fails
|
||||
log_message('error', 'Error fetching permissions: ' . $e->getMessage());
|
||||
// Rethrow the exception to be handled by the caller
|
||||
return static::query()->get();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Error fetching permissions: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch permissions assigned to a specific role
|
||||
*
|
||||
* @param int $role_id The ID of the role
|
||||
* @return array List of permissions associated with the role
|
||||
* @throws \Exception If there is an error during the fetch
|
||||
* Fetch permissions assigned to a specific role.
|
||||
* Returns rows similar to CI:
|
||||
* role_permissions.* + permissions.name
|
||||
*/
|
||||
public function getRolePermissions($role_id)
|
||||
public static function getRolePermissions(int $roleId): array
|
||||
{
|
||||
try {
|
||||
// Query to get role permissions along with default permissions
|
||||
$rolePermissions = $this->db->table('role_permissions')
|
||||
->select('role_permissions.*, permissions.name')
|
||||
// Join with 'permissions' table to get permission details
|
||||
->join('permissions', 'role_permissions.permission_id = permissions.id', 'left')
|
||||
// Filter by the specified role ID
|
||||
->where('role_permissions.role_id', $role_id)
|
||||
->get()
|
||||
// Get the result as an array
|
||||
->getResultArray();
|
||||
// Log the fetched role permissions for debugging
|
||||
log_message('info', 'Role permissions fetched: ' . print_r($rolePermissions, true));
|
||||
return $rolePermissions;
|
||||
} catch (\Exception $e) {
|
||||
// Log the error message if the query fails
|
||||
log_message('error', 'Error fetching role permissions for role ID: ' . $role_id . ': ' . $e->getMessage());
|
||||
// Rethrow the exception to be handled by the caller
|
||||
$rows = DB::table('role_permissions')
|
||||
->leftJoin('permissions', 'role_permissions.permission_id', '=', 'permissions.id')
|
||||
->where('role_permissions.role_id', $roleId)
|
||||
->select('role_permissions.*', 'permissions.name')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
|
||||
Log::info('Role permissions fetched: ' . print_r($rows, true));
|
||||
|
||||
return $rows;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Error fetching role permissions for role ID {$roleId}: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class PlacementBatch extends Model
|
||||
class PlacementBatch extends BaseModel
|
||||
{
|
||||
protected $table = 'placement_batches';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'placement_test',
|
||||
'school_year',
|
||||
@@ -15,5 +19,20 @@ class PlacementBatch extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class PlacementLevel extends Model
|
||||
class PlacementLevel extends BaseModel
|
||||
{
|
||||
protected $table = 'placement_levels';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'level',
|
||||
@@ -16,5 +20,26 @@ class PlacementLevel extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class PlacementScore extends Model
|
||||
class PlacementScore extends BaseModel
|
||||
{
|
||||
protected $table = 'placement_scores';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'batch_id',
|
||||
'student_id',
|
||||
@@ -16,5 +20,33 @@ class PlacementScore extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'batch_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'score' => 'decimal:2', // change to 'integer' if score is whole number
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function batch()
|
||||
{
|
||||
return $this->belongsTo(PlacementBatch::class, 'batch_id');
|
||||
}
|
||||
|
||||
public function student()
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ use App\Models\BaseModel;
|
||||
|
||||
class Preferences extends BaseModel
|
||||
{
|
||||
protected $table = 'user_preferences'; // Adjusted to match the table name
|
||||
protected $primaryKey = 'id'; // Primary key of the table
|
||||
protected $table = 'user_preferences';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
// Allowed fields to enable mass assignment
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'notification_email',
|
||||
@@ -25,8 +26,21 @@ class Preferences extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
// Enable automatic timestamp management if needed
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
|
||||
'receive_email_notifications' => 'boolean',
|
||||
'receive_sms_notifications' => 'boolean',
|
||||
'receive_push_notifications' => 'boolean',
|
||||
'daily_summary_email' => 'boolean',
|
||||
'privacy_mode' => 'boolean',
|
||||
'marketing_emails' => 'boolean',
|
||||
'account_activity_alerts' => 'boolean',
|
||||
];
|
||||
|
||||
/* Optional relationship */
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
|
||||
class PrintRequest extends Model
|
||||
class PrintRequest extends BaseModel
|
||||
{
|
||||
protected $table = 'print_requests';
|
||||
|
||||
// ✅ CI: useTimestamps = true (created_at/updated_at)
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'teacher_id',
|
||||
'admin_id',
|
||||
@@ -20,5 +24,28 @@ class PrintRequest extends Model
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'teacher_id' => 'integer',
|
||||
'admin_id' => 'integer',
|
||||
'class_id' => 'integer',
|
||||
'num_copies' => 'integer',
|
||||
'required_by' => 'datetime', // if stored as DATETIME
|
||||
];
|
||||
|
||||
/* Optional relationships */
|
||||
public function teacher()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'teacher_id');
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
|
||||
public function schoolClass()
|
||||
{
|
||||
return $this->belongsTo(SchoolClass::class, 'class_id');
|
||||
}
|
||||
}
|
||||
+123
-30
@@ -2,13 +2,20 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Project extends BaseModel
|
||||
{
|
||||
/**
|
||||
* Table name (your CI table is singular "project").
|
||||
*/
|
||||
protected $table = 'project';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
|
||||
/**
|
||||
* Mass assignment.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
@@ -22,42 +29,128 @@ class Project extends BaseModel
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* If your table has created_at/updated_at columns (it does),
|
||||
* keep timestamps enabled (default = true).
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
/**
|
||||
* Calculate the average project score for a given student, semester, and school year.
|
||||
*
|
||||
* @param int $studentId The student's ID.
|
||||
* @param string $semester The semester (e.g., 'Fall', 'Spring').
|
||||
* @param string $schoolYear The school year (e.g., '2024-2025').
|
||||
* @return float The average project score, or 0 if no scores are found.
|
||||
* Helpful casts (adjust types if your DB columns differ).
|
||||
*/
|
||||
public function getAverageProjectScore(int $studentId, string $semester, string $schoolYear): float
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'project_index' => 'integer',
|
||||
'score' => 'decimal:2', // or 'float' if you prefer
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional but useful)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function student(): BelongsTo
|
||||
{
|
||||
// Fetch the project scores for the given student, semester, and school year
|
||||
$builder = $this->builder();
|
||||
$builder->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
// Execute the query and get the results
|
||||
$query = $builder->get();
|
||||
$results = $query->getResultArray();
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
// If there are no scores, return 0
|
||||
if (empty($results)) {
|
||||
return 0.0;
|
||||
}
|
||||
public function classSection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
|
||||
// Calculate the average score
|
||||
$totalScore = 0;
|
||||
$scoreCount = count($results);
|
||||
public function school(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(School::class, 'school_id');
|
||||
}
|
||||
|
||||
foreach ($results as $result) {
|
||||
$totalScore += $result['score'];
|
||||
}
|
||||
/* ============================================================
|
||||
* Query Scopes (reusable filters)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
return $totalScore / $scoreCount;
|
||||
public function scopeForStudent(Builder $q, int $studentId): Builder
|
||||
{
|
||||
return $q->where('student_id', $studentId);
|
||||
}
|
||||
|
||||
public function scopeForTerm(Builder $q, string $semester, string $schoolYear): Builder
|
||||
{
|
||||
return $q->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
public function scopeForClassSection(Builder $q, ?int $classSectionId): Builder
|
||||
{
|
||||
return $classSectionId !== null
|
||||
? $q->where('class_section_id', $classSectionId)
|
||||
: $q;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Business: Average score
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate average project score for a student + term (+ optional class section).
|
||||
*
|
||||
* Enhancements vs CI version:
|
||||
* - Uses SQL AVG for performance
|
||||
* - Ignores NULL, empty string, and non-numeric (best-effort) values
|
||||
* - Returns float|null
|
||||
*/
|
||||
public static function averageScore(
|
||||
int $studentId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $classSectionId = null
|
||||
): ?float {
|
||||
$avg = static::query()
|
||||
->forStudent($studentId)
|
||||
->forTerm($semester, $schoolYear)
|
||||
->forClassSection($classSectionId)
|
||||
// ignore NULL and blank
|
||||
->whereNotNull('score')
|
||||
->whereRaw("TRIM(COALESCE(score, '')) <> ''")
|
||||
/**
|
||||
* If score is stored as VARCHAR and may contain junk,
|
||||
* this helps filter numeric-only values in MySQL.
|
||||
* If score is a numeric column, you can remove this line.
|
||||
*/
|
||||
->when(static::scoreMightBeString(), function (Builder $q) {
|
||||
$q->whereRaw("TRIM(score) REGEXP '^-?[0-9]+(\\.[0-9]+)?$'");
|
||||
})
|
||||
->avg('score');
|
||||
|
||||
return $avg === null ? null : (float) $avg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance method (so you can keep a similar calling style to CI).
|
||||
*/
|
||||
public function getAverageProjectScore(
|
||||
int $studentId,
|
||||
string $semester,
|
||||
string $schoolYear,
|
||||
?int $classSectionId = null
|
||||
): ?float {
|
||||
return static::averageScore($studentId, $semester, $schoolYear, $classSectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the REGEXP filter depending on your schema.
|
||||
* If your `score` column is DECIMAL/INT/FLOAT, return false.
|
||||
*/
|
||||
protected static function scoreMightBeString(): bool
|
||||
{
|
||||
// Safe default if you're unsure. Set to false if score is numeric.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PromotionQueue extends BaseModel
|
||||
{
|
||||
protected $table = 'promotion_queue';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'from_class_section_id',
|
||||
@@ -20,25 +22,97 @@ class PromotionQueue extends BaseModel
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
public function upsert(array $data): bool
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'from_class_section_id' => 'integer',
|
||||
'to_class_id' => 'integer',
|
||||
'to_class_section_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function student(): BelongsTo
|
||||
{
|
||||
if (empty($data['student_id']) || empty($data['school_year_to'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$existing = $this->where('student_id', (int)$data['student_id'])
|
||||
->where('school_year_to', (string)$data['school_year_to'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return (bool) $this->update((int)$existing[$this->primaryKey], array_merge($existing, $data));
|
||||
}
|
||||
|
||||
return (bool) $this->insert($data);
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
}
|
||||
|
||||
public function fromClassSection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'from_class_section_id');
|
||||
}
|
||||
|
||||
public function toClassSection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'to_class_section_id');
|
||||
}
|
||||
|
||||
public function toClass(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SchoolClass::class, 'to_class_id'); // rename model if yours is ClassModel/ClassRoom/etc.
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Scopes (reusable filters)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function scopeForStudent(Builder $q, int $studentId): Builder
|
||||
{
|
||||
return $q->where('student_id', $studentId);
|
||||
}
|
||||
|
||||
public function scopeForSchoolYearTo(Builder $q, string $schoolYearTo): Builder
|
||||
{
|
||||
return $q->where('school_year_to', $schoolYearTo);
|
||||
}
|
||||
|
||||
public function scopeStatus(Builder $q, string $status): Builder
|
||||
{
|
||||
return $q->where('status', $status);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Upsert (Laravel style)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* True upsert by (student_id, school_year_to).
|
||||
*
|
||||
* Returns the saved model, or null if required keys missing.
|
||||
*/
|
||||
public static function upsertQueue(array $data): ?self
|
||||
{
|
||||
$studentId = isset($data['student_id']) ? (int) $data['student_id'] : null;
|
||||
$yearTo = isset($data['school_year_to']) ? (string) $data['school_year_to'] : null;
|
||||
|
||||
if (!$studentId || !$yearTo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only update columns that are allowed.
|
||||
$payload = array_intersect_key($data, array_flip((new static)->getFillable()));
|
||||
|
||||
return static::updateOrCreate(
|
||||
['student_id' => $studentId, 'school_year_to' => $yearTo],
|
||||
$payload
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional: keep the same CI signature (bool result).
|
||||
*/
|
||||
public static function upsertBool(array $data): bool
|
||||
{
|
||||
return (bool) static::upsertQueue($data);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class PurchaseOrder extends BaseModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'purchase_orders';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'po_number',
|
||||
'supplier_id',
|
||||
@@ -19,12 +25,136 @@ class PurchaseOrder extends BaseModel
|
||||
'total',
|
||||
'notes',
|
||||
];
|
||||
public $timestamps = true;
|
||||
protected $useSoftDeletes = true;
|
||||
|
||||
protected $validationRules = [
|
||||
'po_number' => 'required|max_length[60]|is_unique[purchase_orders.po_number,id,{id}]',
|
||||
'supplier_id' => 'required|is_natural_no_zero',
|
||||
'status' => 'in_list[draft,ordered,received,canceled]',
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'supplier_id' => 'integer',
|
||||
'order_date' => 'date', // or 'datetime' if your DB stores time too
|
||||
'expected_date' => 'date',
|
||||
'subtotal' => 'decimal:2',
|
||||
'tax' => 'decimal:2',
|
||||
'total' => 'decimal:2',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Status (enum-like)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_ORDERED = 'ordered';
|
||||
public const STATUS_RECEIVED = 'received';
|
||||
public const STATUS_CANCELED = 'canceled';
|
||||
|
||||
public static function allowedStatuses(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_ORDERED,
|
||||
self::STATUS_RECEIVED,
|
||||
self::STATUS_CANCELED,
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Relationships
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function supplier(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Supplier::class, 'supplier_id');
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(PurchaseOrderItem::class, 'purchase_order_id');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Scopes
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function scopeStatus(Builder $q, string $status): Builder
|
||||
{
|
||||
return $q->where('status', $status);
|
||||
}
|
||||
|
||||
public function scopeOpen(Builder $q): Builder
|
||||
{
|
||||
return $q->whereIn('status', [self::STATUS_DRAFT, self::STATUS_ORDERED]);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers / computed totals
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Recalculate subtotal/tax/total from items.
|
||||
* Tax rate is optional. If you store tax as absolute amount, pass null and keep existing tax.
|
||||
*/
|
||||
public function recalcTotals(?float $taxRate = null): self
|
||||
{
|
||||
$subtotal = (float) $this->items()
|
||||
->selectRaw('COALESCE(SUM(quantity * unit_cost), 0) as s')
|
||||
->value('s');
|
||||
|
||||
$tax = $taxRate === null
|
||||
? (float) ($this->tax ?? 0)
|
||||
: round($subtotal * $taxRate, 2);
|
||||
|
||||
$total = $subtotal + $tax;
|
||||
|
||||
$this->subtotal = $subtotal;
|
||||
$this->tax = $tax;
|
||||
$this->total = $total;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markOrdered(): self
|
||||
{
|
||||
$this->status = self::STATUS_ORDERED;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markReceived(): self
|
||||
{
|
||||
$this->status = self::STATUS_RECEIVED;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markCanceled(): self
|
||||
{
|
||||
$this->status = self::STATUS_CANCELED;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Validation rules (controller/FormRequest reuse)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public static function rules(?int $id = null): array
|
||||
{
|
||||
// Mirrors CI: unique purchase_orders.po_number except current id.
|
||||
return [
|
||||
'po_number' => ['required', 'string', 'max:60', 'unique:purchase_orders,po_number,' . ($id ?? 'NULL') . ',id'],
|
||||
'supplier_id' => ['required', 'integer', 'min:1', 'exists:suppliers,id'],
|
||||
'status' => ['nullable', 'string', 'in:' . implode(',', self::allowedStatuses())],
|
||||
|
||||
'order_date' => ['nullable', 'date'],
|
||||
'expected_date' => ['nullable', 'date', 'after_or_equal:order_date'],
|
||||
'subtotal' => ['nullable', 'numeric', 'min:0'],
|
||||
'tax' => ['nullable', 'numeric', 'min:0'],
|
||||
'total' => ['nullable', 'numeric', 'min:0'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PurchaseOrderItem extends BaseModel
|
||||
{
|
||||
protected $table = 'purchase_order_items';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'purchase_order_id',
|
||||
'supply_id',
|
||||
@@ -16,12 +17,71 @@ class PurchaseOrderItem extends BaseModel
|
||||
'received_qty',
|
||||
'unit_cost',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $validationRules = [
|
||||
'purchase_order_id' => 'required|is_natural_no_zero',
|
||||
'supply_id' => 'required|is_natural_no_zero',
|
||||
'quantity' => 'required|is_natural_no_zero',
|
||||
'unit_cost' => 'required|decimal',
|
||||
protected $casts = [
|
||||
'purchase_order_id' => 'integer',
|
||||
'supply_id' => 'integer',
|
||||
'quantity' => 'integer',
|
||||
'received_qty' => 'integer',
|
||||
'unit_cost' => 'decimal:2', // adjust scale to match DB (e.g., decimal:4)
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional but recommended)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function purchaseOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PurchaseOrder::class, 'purchase_order_id');
|
||||
}
|
||||
|
||||
public function supply(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Supply::class, 'supply_id');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Accessors (handy for totals / UI)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
// total ordered cost
|
||||
public function getLineTotalAttribute(): float
|
||||
{
|
||||
$qty = (int) ($this->quantity ?? 0);
|
||||
$cost = (float) ($this->unit_cost ?? 0);
|
||||
return $qty * $cost;
|
||||
}
|
||||
|
||||
// total received cost
|
||||
public function getReceivedLineTotalAttribute(): float
|
||||
{
|
||||
$qty = (int) ($this->received_qty ?? 0);
|
||||
$cost = (float) ($this->unit_cost ?? 0);
|
||||
return $qty * $cost;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Validation rules (use in FormRequest or controller)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public static function rules(bool $updating = false): array
|
||||
{
|
||||
// if updating, you might allow partial updates (sometimes)
|
||||
// keep strict by default to match CI behavior
|
||||
return [
|
||||
'purchase_order_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:purchase_orders,id'],
|
||||
'supply_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:supplies,id'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'quantity' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'],
|
||||
'received_qty' => ['nullable', 'integer', 'min:0', 'lte:quantity'],
|
||||
'unit_cost' => [$updating ? 'sometimes' : 'required', 'numeric', 'min:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+20
-67
@@ -3,14 +3,14 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Quiz extends BaseModel
|
||||
{
|
||||
protected $table = 'quiz';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable = [
|
||||
'student_id',
|
||||
'school_id',
|
||||
@@ -25,71 +25,24 @@ class Quiz extends BaseModel
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
protected bool $allowEmptyInserts = false;
|
||||
protected bool $updateOnlyChanged = true;
|
||||
protected $casts = [
|
||||
'student_id' => 'integer',
|
||||
'school_id' => 'integer',
|
||||
'class_section_id' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
'quiz_index' => 'integer',
|
||||
'score' => 'decimal:2',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $casts = [];
|
||||
protected $castHandlers = [];
|
||||
|
||||
// Dates
|
||||
public $timestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $deletedField = 'deleted_at';
|
||||
|
||||
// Validation
|
||||
protected $validationRules = [];
|
||||
protected $validationMessages = [];
|
||||
protected $skipValidation = false;
|
||||
protected $cleanValidationRules = true;
|
||||
|
||||
// Callbacks
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = [];
|
||||
protected $afterInsert = [];
|
||||
protected $beforeUpdate = [];
|
||||
protected $afterUpdate = [];
|
||||
protected $beforeFind = [];
|
||||
protected $afterFind = [];
|
||||
protected $beforeDelete = [];
|
||||
protected $afterDelete = [];
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the average quiz score for a given student, semester, and school year.
|
||||
*
|
||||
* @param int $studentId The student's ID.
|
||||
* @param string $semester The semester (e.g., 'Fall', 'Spring').
|
||||
* @param string $schoolYear The school year (e.g., '2024-2025').
|
||||
* @return float The average quiz score, or 0 if no scores are found.
|
||||
*/
|
||||
public function getAverageQuizScore(int $studentId, string $semester, string $schoolYear): float
|
||||
public function student(): BelongsTo
|
||||
{
|
||||
// Fetch the quiz scores for the given student, semester, and school year
|
||||
$builder = $this->builder();
|
||||
$builder->select('score')
|
||||
->where('student_id', $studentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear);
|
||||
|
||||
// Execute the query and get the results
|
||||
$query = $builder->get();
|
||||
$results = $query->getResultArray();
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
// If there are no scores, return 0
|
||||
if (empty($results)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Calculate the average score
|
||||
$totalScore = 0;
|
||||
$scoreCount = count($results);
|
||||
|
||||
foreach ($results as $result) {
|
||||
$totalScore += $result['score'];
|
||||
}
|
||||
|
||||
return $totalScore / $scoreCount;
|
||||
public function classSection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClassSection::class, 'class_section_id');
|
||||
}
|
||||
}
|
||||
|
||||
+139
-18
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Refund extends BaseModel
|
||||
{
|
||||
protected $table = 'refunds';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'school_year',
|
||||
@@ -30,27 +32,146 @@ class Refund extends BaseModel
|
||||
'updated_at',
|
||||
'semester',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'approved_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
|
||||
'refund_amount' => 'decimal:2',
|
||||
'refund_paid_amount' => 'decimal:2',
|
||||
|
||||
'requested_at' => 'datetime',
|
||||
'approved_at' => 'datetime',
|
||||
'refunded_at' => 'datetime',
|
||||
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Status (enum-like)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public const STATUS_PENDING = 'Pending';
|
||||
public const STATUS_APPROVED = 'Approved';
|
||||
public const STATUS_PARTIAL = 'Partial';
|
||||
public const STATUS_PAID = 'Paid';
|
||||
public const STATUS_REJECTED = 'Rejected';
|
||||
public const STATUS_CANCELED = 'Canceled';
|
||||
|
||||
public static function paidOutStatuses(): array
|
||||
{
|
||||
// matches your CI whereIn(['Partial','Paid'])
|
||||
return [self::STATUS_PARTIAL, self::STATUS_PAID];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ParentModel::class, 'parent_id'); // rename to your real model, e.g. ParentUser/Guardian/Customer
|
||||
}
|
||||
|
||||
public function invoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'invoice_id');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Scopes
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function scopeForParent(Builder $q, int $parentId): Builder
|
||||
{
|
||||
return $q->where('parent_id', $parentId);
|
||||
}
|
||||
|
||||
public function scopeForSchoolYear(Builder $q, string $schoolYear): Builder
|
||||
{
|
||||
return $q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
public function scopePaidOut(Builder $q): Builder
|
||||
{
|
||||
return $q->whereIn('status', self::paidOutStatuses());
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Business: totals
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get total approved refund for a parent in a specific school year.
|
||||
*
|
||||
* @param int $parentId
|
||||
* @param string $schoolYear
|
||||
* @return float
|
||||
* Total paid-out refunds for a parent in a specific school year (Partial + Paid).
|
||||
* Mirrors the CI behavior and returns 0.00 if none.
|
||||
*/
|
||||
public static function totalApprovedPaidOutForParentAndYear(int $parentId, string $schoolYear): float
|
||||
{
|
||||
$sum = static::query()
|
||||
->forParent($parentId)
|
||||
->forSchoolYear($schoolYear)
|
||||
->paidOut()
|
||||
->sum('refund_paid_amount');
|
||||
|
||||
return (float) $sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance-style method if you want to keep a similar calling style to CI.
|
||||
*/
|
||||
public function getTotalApprovedRefundByParentIdAndSchoolYear(int $parentId, string $schoolYear): float
|
||||
{
|
||||
// Sum refunds that have been at least partially paid out to the parent
|
||||
$result = $this->selectSum('refund_paid_amount', 'total_paid')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->whereIn('status', ['Partial', 'Paid'])
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
return $result && isset($result['total_paid']) ? (float) $result['total_paid'] : 0.00;
|
||||
return static::totalApprovedPaidOutForParentAndYear($parentId, $schoolYear);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Validation rules (optional helper for controllers/FormRequests)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public static function rules(?int $id = null): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => ['required', 'integer', 'min:1', 'exists:parents,id'], // adjust table name
|
||||
'school_year' => ['required', 'string', 'max:20'],
|
||||
'invoice_id' => ['nullable', 'integer', 'exists:invoices,id'],
|
||||
|
||||
'refund_amount' => ['required', 'numeric', 'min:0'],
|
||||
'refund_paid_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
|
||||
'requested_at' => ['nullable', 'date'],
|
||||
'approved_at' => ['nullable', 'date', 'after_or_equal:requested_at'],
|
||||
'refunded_at' => ['nullable', 'date', 'after_or_equal:approved_at'],
|
||||
|
||||
'status' => ['required', 'string', 'in:' . implode(',', [
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_PARTIAL,
|
||||
self::STATUS_PAID,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_CANCELED,
|
||||
])],
|
||||
|
||||
'reason' => ['nullable', 'string', 'max:2000'],
|
||||
'request' => ['nullable', 'string'],
|
||||
'note' => ['nullable', 'string', 'max:5000'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
|
||||
'approved_by' => ['nullable', 'integer'],
|
||||
'updated_by' => ['nullable', 'integer'],
|
||||
|
||||
'refund_method' => ['nullable', 'string', 'max:50'], // cash/check/cc/etc.
|
||||
'check_nbr' => ['nullable', 'string', 'max:80'],
|
||||
'check_file' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Reimbursement extends BaseModel
|
||||
{
|
||||
protected $table = 'reimbursements';
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $fillable = [
|
||||
'expense_id',
|
||||
'amount',
|
||||
@@ -25,6 +27,146 @@ class Reimbursement extends BaseModel
|
||||
'reimbursement_method',
|
||||
'batch_number',
|
||||
];
|
||||
|
||||
/**
|
||||
* If your table has created_at/updated_at, keep timestamps (default).
|
||||
* If it does NOT, set to false.
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
}
|
||||
protected $casts = [
|
||||
'amount' => 'decimal:2',
|
||||
'expense_id' => 'integer',
|
||||
'approved_by' => 'integer',
|
||||
'added_by' => 'integer',
|
||||
'batch_number' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Status (align these strings to your DB values)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_APPROVED = 'approved';
|
||||
public const STATUS_REIMBURSED = 'reimbursed';
|
||||
public const STATUS_REJECTED = 'rejected';
|
||||
|
||||
public static function allowedStatuses(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REIMBURSED,
|
||||
self::STATUS_REJECTED,
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function expense(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Expense::class, 'expense_id');
|
||||
}
|
||||
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
// Change Admin::class to your actual admin/user model
|
||||
return $this->belongsTo(Admin::class, 'approved_by');
|
||||
}
|
||||
|
||||
public function addedBy(): BelongsTo
|
||||
{
|
||||
// Change Admin::class to your actual admin/user model
|
||||
return $this->belongsTo(Admin::class, 'added_by');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Scopes
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function scopeForSchoolYear(Builder $q, string $schoolYear): Builder
|
||||
{
|
||||
return $q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
public function scopeForSemester(Builder $q, string $semester): Builder
|
||||
{
|
||||
return $q->where('semester', $semester);
|
||||
}
|
||||
|
||||
public function scopeStatus(Builder $q, string $status): Builder
|
||||
{
|
||||
return $q->where('status', $status);
|
||||
}
|
||||
|
||||
public function scopeInBatch(Builder $q, int $batchNumber): Builder
|
||||
{
|
||||
return $q->where('batch_number', $batchNumber);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function markApproved(int $adminId): self
|
||||
{
|
||||
$this->status = self::STATUS_APPROVED;
|
||||
$this->approved_by = $adminId;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markReimbursed(?string $method = null, ?string $checkNumber = null): self
|
||||
{
|
||||
$this->status = self::STATUS_REIMBURSED;
|
||||
|
||||
if ($method !== null) {
|
||||
$this->reimbursement_method = $method;
|
||||
}
|
||||
if ($checkNumber !== null) {
|
||||
$this->check_number = $checkNumber;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Optional validation helper (FormRequest/controller)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public static function rules(bool $updating = false): array
|
||||
{
|
||||
return [
|
||||
'amount' => [$updating ? 'sometimes' : 'required', 'numeric', 'min:0'],
|
||||
'reimbursed_to' => ['nullable', 'string', 'max:255'],
|
||||
'expense_id' => ['nullable', 'integer', 'exists:expenses,id'],
|
||||
'description' => ['nullable', 'string', 'max:5000'],
|
||||
'status' => ['nullable', 'string', 'in:' . implode(',', self::allowedStatuses())],
|
||||
'receipt_path' => ['nullable', 'string', 'max:255'],
|
||||
|
||||
'approved_by' => ['nullable', 'integer'],
|
||||
'added_by' => ['nullable', 'integer'],
|
||||
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
|
||||
'check_number' => ['nullable', 'string', 'max:80'],
|
||||
'reimbursement_method' => ['nullable', 'string', 'max:50'],
|
||||
'batch_number' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,20 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ReimbursementBatch extends Model
|
||||
class ReimbursementBatch extends BaseModel
|
||||
{
|
||||
protected $table = 'reimbursement_batches';
|
||||
|
||||
/**
|
||||
* Your table uses opened_at/closed_at (no created_at/updated_at).
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'yearly_batch_number',
|
||||
@@ -19,5 +28,143 @@ class ReimbursementBatch extends Model
|
||||
'closed_at',
|
||||
'notes',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'created_by' => 'integer',
|
||||
'closed_by' => 'integer',
|
||||
'yearly_batch_number' => 'integer',
|
||||
'opened_at' => 'datetime',
|
||||
'closed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Status (enum-like)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public const STATUS_OPEN = 'open';
|
||||
public const STATUS_CLOSED = 'closed';
|
||||
public const STATUS_DRAFT = 'draft';
|
||||
|
||||
public static function allowedStatuses(): array
|
||||
{
|
||||
return [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_OPEN,
|
||||
self::STATUS_CLOSED,
|
||||
];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional but useful)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReimbursementBatchItem::class, 'batch_id');
|
||||
}
|
||||
|
||||
public function adminFiles(): HasMany
|
||||
{
|
||||
return $this->hasMany(ReimbursementBatchAdminFile::class, 'batch_id');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
// Change Admin::class to your real admin/user model
|
||||
return $this->belongsTo(Admin::class, 'created_by');
|
||||
}
|
||||
|
||||
public function closer(): BelongsTo
|
||||
{
|
||||
// Change Admin::class to your real admin/user model
|
||||
return $this->belongsTo(Admin::class, 'closed_by');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Scopes
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function scopeForSchoolYear(Builder $q, string $schoolYear): Builder
|
||||
{
|
||||
return $q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
public function scopeForSemester(Builder $q, string $semester): Builder
|
||||
{
|
||||
return $q->where('semester', $semester);
|
||||
}
|
||||
|
||||
public function scopeStatus(Builder $q, string $status): Builder
|
||||
{
|
||||
return $q->where('status', $status);
|
||||
}
|
||||
|
||||
public function scopeOpen(Builder $q): Builder
|
||||
{
|
||||
return $q->where('status', self::STATUS_OPEN);
|
||||
}
|
||||
|
||||
public function scopeClosed(Builder $q): Builder
|
||||
{
|
||||
return $q->where('status', self::STATUS_CLOSED);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function open(int $adminId, ?string $notes = null): self
|
||||
{
|
||||
$this->status = self::STATUS_OPEN;
|
||||
$this->opened_at = $this->opened_at ?: now();
|
||||
$this->created_by = $this->created_by ?: $adminId;
|
||||
|
||||
if ($notes !== null) {
|
||||
$this->notes = $notes;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function close(int $adminId, ?string $notes = null): self
|
||||
{
|
||||
$this->status = self::STATUS_CLOSED;
|
||||
$this->closed_at = now();
|
||||
$this->closed_by = $adminId;
|
||||
|
||||
if ($notes !== null) {
|
||||
$this->notes = $notes;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Optional validation helper (FormRequest/controller)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public static function rules(?int $id = null): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'status' => ['required', 'string', 'in:' . implode(',', self::allowedStatuses())],
|
||||
'created_by' => ['nullable', 'integer'],
|
||||
'closed_by' => ['nullable', 'integer'],
|
||||
'opened_at' => ['nullable', 'date'],
|
||||
'closed_at' => ['nullable', 'date', 'after_or_equal:opened_at'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
'yearly_batch_number' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReimbursementBatchAdminFile extends Model
|
||||
class ReimbursementBatchAdminFile extends BaseModel
|
||||
{
|
||||
protected $table = 'reimbursement_batch_admin_files';
|
||||
|
||||
/**
|
||||
* Your table uses uploaded_at (not created_at/updated_at).
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'batch_id',
|
||||
'admin_id',
|
||||
@@ -15,5 +22,65 @@ class ReimbursementBatchAdminFile extends Model
|
||||
'uploaded_at',
|
||||
'uploaded_by',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'batch_id' => 'integer',
|
||||
'admin_id' => 'integer',
|
||||
'uploaded_by' => 'integer',
|
||||
'uploaded_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional but recommended)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function batch(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ReimbursementBatch::class, 'batch_id');
|
||||
}
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
// Change Admin to your actual user/admin model if different
|
||||
return $this->belongsTo(Admin::class, 'admin_id');
|
||||
}
|
||||
|
||||
public function uploader(): BelongsTo
|
||||
{
|
||||
// If uploaded_by references the same admins/users table
|
||||
return $this->belongsTo(Admin::class, 'uploaded_by');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Convenience helpers
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an admin file row and auto-set uploaded_at if missing.
|
||||
*/
|
||||
public static function createForBatch(array $data): self
|
||||
{
|
||||
if (empty($data['uploaded_at'])) {
|
||||
$data['uploaded_at'] = now();
|
||||
}
|
||||
|
||||
return static::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional: validation rules helper (use in FormRequest/controller).
|
||||
*/
|
||||
public static function rules(bool $updating = false): array
|
||||
{
|
||||
return [
|
||||
'batch_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:reimbursement_batches,id'],
|
||||
'admin_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:admins,id'], // adjust table
|
||||
'filename' => [$updating ? 'sometimes' : 'required', 'string', 'max:255'],
|
||||
'original_filename' => ['nullable', 'string', 'max:255'],
|
||||
'uploaded_at' => ['nullable', 'date'],
|
||||
'uploaded_by' => ['nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReimbursementBatchItem extends Model
|
||||
class ReimbursementBatchItem extends BaseModel
|
||||
{
|
||||
protected $table = 'reimbursement_batch_items';
|
||||
|
||||
/**
|
||||
* Table uses assigned_at/unassigned_at (no created_at/updated_at).
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'batch_id',
|
||||
'expense_id',
|
||||
@@ -18,5 +26,126 @@ class ReimbursementBatchItem extends Model
|
||||
'school_year',
|
||||
'semester',
|
||||
];
|
||||
public $timestamps = false;
|
||||
|
||||
protected $casts = [
|
||||
'batch_id' => 'integer',
|
||||
'expense_id' => 'integer',
|
||||
'reimbursement_id' => 'integer',
|
||||
'admin_id' => 'integer',
|
||||
'assigned_at' => 'datetime',
|
||||
'unassigned_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function batch(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ReimbursementBatch::class, 'batch_id');
|
||||
}
|
||||
|
||||
public function expense(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Expense::class, 'expense_id');
|
||||
}
|
||||
|
||||
public function reimbursement(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Reimbursement::class, 'reimbursement_id');
|
||||
}
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
// Change to User/Admin model used in your app
|
||||
return $this->belongsTo(Admin::class, 'admin_id');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Scopes
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function scopeForBatch(Builder $q, int $batchId): Builder
|
||||
{
|
||||
return $q->where('batch_id', $batchId);
|
||||
}
|
||||
|
||||
public function scopeForSchoolYear(Builder $q, string $schoolYear): Builder
|
||||
{
|
||||
return $q->where('school_year', $schoolYear);
|
||||
}
|
||||
|
||||
public function scopeForSemester(Builder $q, string $semester): Builder
|
||||
{
|
||||
return $q->where('semester', $semester);
|
||||
}
|
||||
|
||||
public function scopeAssigned(Builder $q): Builder
|
||||
{
|
||||
return $q->whereNotNull('admin_id')
|
||||
->whereNotNull('assigned_at')
|
||||
->whereNull('unassigned_at');
|
||||
}
|
||||
|
||||
public function scopeUnassigned(Builder $q): Builder
|
||||
{
|
||||
return $q->whereNull('admin_id')
|
||||
->whereNull('assigned_at');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function assignToAdmin(int $adminId, ?string $notes = null): self
|
||||
{
|
||||
$this->admin_id = $adminId;
|
||||
$this->assigned_at = $this->assigned_at ?: now();
|
||||
$this->unassigned_at = null;
|
||||
|
||||
if ($notes !== null) {
|
||||
$this->notes = $notes;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unassign(?string $notes = null): self
|
||||
{
|
||||
$this->admin_id = null;
|
||||
$this->unassigned_at = now();
|
||||
|
||||
if ($notes !== null) {
|
||||
$this->notes = $notes;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Optional validation helper (FormRequest/controller)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public static function rules(bool $updating = false): array
|
||||
{
|
||||
return [
|
||||
'batch_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:reimbursement_batches,id'],
|
||||
'expense_id' => ['nullable', 'integer', 'exists:expenses,id'],
|
||||
'reimbursement_id' => ['nullable', 'integer', 'exists:reimbursements,id'],
|
||||
'admin_id' => ['nullable', 'integer'], // add exists rule if you know the table
|
||||
'assigned_at' => ['nullable', 'date'],
|
||||
'unassigned_at' => ['nullable', 'date', 'after_or_equal:assigned_at'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ReportCardAcknowledgement extends BaseModel
|
||||
{
|
||||
protected $table = 'report_card_acknowledgements';
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'student_id',
|
||||
'school_year',
|
||||
'semester',
|
||||
'viewed_at',
|
||||
'signed_at',
|
||||
'signed_name',
|
||||
'signer_ip',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'student_id' => 'integer',
|
||||
'viewed_at' => 'datetime',
|
||||
'signed_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/* ============================================================
|
||||
* Relationships (optional)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
// Change to your actual parent model/table (often User/Parent/Guardian)
|
||||
return $this->belongsTo(ParentModel::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function student(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Student::class, 'student_id');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Scopes
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public function scopeForParent(Builder $q, int $parentId): Builder
|
||||
{
|
||||
return $q->where('parent_id', $parentId);
|
||||
}
|
||||
|
||||
public function scopeForStudent(Builder $q, int $studentId): Builder
|
||||
{
|
||||
return $q->where('student_id', $studentId);
|
||||
}
|
||||
|
||||
public function scopeForTerm(Builder $q, string $schoolYear, string $semester): Builder
|
||||
{
|
||||
return $q->where('school_year', $schoolYear)
|
||||
->where('semester', $semester);
|
||||
}
|
||||
|
||||
public function scopeSigned(Builder $q): Builder
|
||||
{
|
||||
return $q->whereNotNull('signed_at');
|
||||
}
|
||||
|
||||
public function scopeViewed(Builder $q): Builder
|
||||
{
|
||||
return $q->whereNotNull('viewed_at');
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Helpers
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mark as viewed (idempotent).
|
||||
*/
|
||||
public function markViewed(?string $ip = null): self
|
||||
{
|
||||
if ($this->viewed_at === null) {
|
||||
$this->viewed_at = now();
|
||||
}
|
||||
|
||||
// optional: store IP even on view (only if signer_ip is empty)
|
||||
if ($ip !== null && empty($this->signer_ip)) {
|
||||
$this->signer_ip = $ip;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark as signed (idempotent).
|
||||
*/
|
||||
public function markSigned(string $signedName, ?string $ip = null): self
|
||||
{
|
||||
$this->signed_name = $signedName;
|
||||
|
||||
if ($this->signed_at === null) {
|
||||
$this->signed_at = now();
|
||||
}
|
||||
|
||||
if ($ip !== null) {
|
||||
$this->signer_ip = $ip;
|
||||
}
|
||||
|
||||
// If they signed, it was definitely viewed.
|
||||
if ($this->viewed_at === null) {
|
||||
$this->viewed_at = now();
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Optional validation helper (FormRequest/controller)
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
public static function rules(bool $updating = false): array
|
||||
{
|
||||
return [
|
||||
'parent_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'],
|
||||
'student_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'],
|
||||
'school_year' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'],
|
||||
'semester' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'],
|
||||
|
||||
'viewed_at' => ['nullable', 'date'],
|
||||
'signed_at' => ['nullable', 'date', 'after_or_equal:viewed_at'],
|
||||
'signed_name' => ['nullable', 'string', 'max:255'],
|
||||
'signer_ip' => ['nullable', 'string', 'max:45'], // IPv4/IPv6
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user