fix tables to have school year, and remove school year from other tables.
API CI/CD / Validate (composer + pint) (push) Successful in 3m6s
API CI/CD / Test (PHPUnit) (push) Failing after 5m48s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 1m33s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
API CI/CD / Validate (composer + pint) (push) Successful in 3m6s
API CI/CD / Test (PHPUnit) (push) Failing after 5m48s
API CI/CD / Build frontend assets (push) Successful in 1m2s
API CI/CD / Security audit (push) Failing after 1m33s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
trait BelongsToSchoolYear
|
||||
{
|
||||
public function scopeForSchoolYear(Builder $query, string $schoolYear): Builder
|
||||
{
|
||||
return $query->where($this->getTable().'.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
public function scopeForSchoolYearContext(Builder $query, object $context): Builder
|
||||
{
|
||||
return $query->where($this->getTable().'.school_year', $context->yearName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
trait HasOptionalSchoolYearContext
|
||||
{
|
||||
public function scopeForOptionalSchoolYearContext(Builder $query, object $context): Builder
|
||||
{
|
||||
return $query->where(function (Builder $q) use ($context) {
|
||||
$q->whereNull($this->getTable().'.school_year')
|
||||
->orWhere($this->getTable().'.school_year', $context->yearName());
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeOnlyForSchoolYearContext(Builder $query, object $context): Builder
|
||||
{
|
||||
return $query->where($this->getTable().'.school_year', $context->yearName());
|
||||
}
|
||||
}
|
||||
+6
-43
@@ -61,7 +61,7 @@ class Staff extends BaseModel
|
||||
|
||||
public function scopeForSchoolYear(Builder $q, string $schoolYear): Builder
|
||||
{
|
||||
return $q->where('school_year', $schoolYear);
|
||||
return $q;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -88,8 +88,7 @@ class Staff extends BaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a staff row keyed by (user_id, school_year).
|
||||
* - If school_year missing: reuse latest existing school_year for that user if available.
|
||||
* Insert or update a staff row keyed by user_id.
|
||||
* - created_at is preserved on update (unless explicitly provided).
|
||||
* - updated_at can be provided by caller; otherwise we set it.
|
||||
*
|
||||
@@ -103,45 +102,10 @@ class Staff extends BaseModel
|
||||
|
||||
$userId = (int) $data['user_id'];
|
||||
|
||||
// Resolve school_year fallback like your legacy logic
|
||||
$schoolYear = $data['school_year'] ?? null;
|
||||
if ($schoolYear === null) {
|
||||
$existingAny = static::query()
|
||||
->where('user_id', $userId)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if ($existingAny && $existingAny->school_year) {
|
||||
$schoolYear = (string) $existingAny->school_year;
|
||||
$data['school_year'] = $schoolYear;
|
||||
}
|
||||
}
|
||||
|
||||
// Find existing by key
|
||||
$existing = null;
|
||||
if ($schoolYear !== null) {
|
||||
$existing = static::query()
|
||||
->where('user_id', $userId)
|
||||
->where('school_year', (string) $schoolYear)
|
||||
->first();
|
||||
} else {
|
||||
// If no school_year, treat (user_id) as key
|
||||
$existing = static::query()
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
}
|
||||
|
||||
// The table enforces unique email addresses, so a user cannot safely
|
||||
// have multiple staff rows across different school years with the same
|
||||
// generated email. If an exact school-year row is missing but the user
|
||||
// already has any staff row, update that existing row instead of trying
|
||||
// to insert a duplicate-email record.
|
||||
if (! $existing) {
|
||||
$existing = static::query()
|
||||
->where('user_id', $userId)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
$existing = static::query()
|
||||
->where('user_id', $userId)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$now = now(); // use UTC if app.timezone=UTC (recommended)
|
||||
|
||||
@@ -201,7 +165,6 @@ class Staff extends BaseModel
|
||||
'role_name' => ['nullable', 'string', 'max:80'],
|
||||
'active_role' => ['nullable', 'string', 'max:80'], // includes 'inactive'
|
||||
'status' => ['nullable', 'string', 'max:40'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'created_at' => ['nullable', 'date'],
|
||||
'updated_at' => ['nullable', 'date'],
|
||||
];
|
||||
|
||||
+13
-3
@@ -99,7 +99,12 @@ class Student extends BaseModel
|
||||
return $q;
|
||||
}
|
||||
|
||||
return $q->where('school_year', $schoolYear);
|
||||
return $q->whereExists(function ($sub) use ($schoolYear) {
|
||||
$sub->selectRaw('1')
|
||||
->from('student_class as sc')
|
||||
->whereColumn('sc.student_id', 'students.id')
|
||||
->where('sc.school_year', $schoolYear);
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeOrderByName(Builder $q): Builder
|
||||
@@ -197,9 +202,14 @@ class Student extends BaseModel
|
||||
$q->whereIn('students.is_new', [0, 1]);
|
||||
}
|
||||
|
||||
// school_year filter (skip if null or "all")
|
||||
// Students are global identities; year rosters come from yearly class/enrollment rows.
|
||||
if ($schoolYear !== null && strtolower($schoolYear) !== 'all') {
|
||||
$q->where('students.school_year', $schoolYear);
|
||||
$q->whereExists(function ($sub) use ($schoolYear) {
|
||||
$sub->selectRaw('1')
|
||||
->from('student_class as sc')
|
||||
->whereColumn('sc.student_id', 'students.id')
|
||||
->where('sc.school_year', $schoolYear);
|
||||
});
|
||||
}
|
||||
|
||||
return $q->groupBy('students.id')
|
||||
|
||||
+9
-32
@@ -172,10 +172,6 @@ class User extends Authenticatable implements JWTSubject
|
||||
$pivotColumns[] = 'deleted_at';
|
||||
$relation->wherePivotNull('deleted_at');
|
||||
}
|
||||
if (Schema::hasColumn('user_roles', 'school_year')) {
|
||||
$pivotColumns[] = 'school_year';
|
||||
}
|
||||
|
||||
if (! empty($pivotColumns)) {
|
||||
$relation->withPivot($pivotColumns);
|
||||
}
|
||||
@@ -422,7 +418,6 @@ class User extends Authenticatable implements JWTSubject
|
||||
->join('user_roles', 'user_roles.user_id', '=', 'users.id')
|
||||
->join('roles', 'roles.id', '=', 'user_roles.role_id')
|
||||
->where('roles.name', $roleName)
|
||||
->where('users.school_year', $schoolYear)
|
||||
->whereNull('user_roles.deleted_at')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
@@ -440,7 +435,6 @@ class User extends Authenticatable implements JWTSubject
|
||||
->selectRaw('u.id, MAX(CASE WHEN r.slug NOT IN ("guest","teacher","teacher_assistant","parent") THEN 1 ELSE 0 END) as is_admin')
|
||||
->join('user_roles as ur', 'ur.user_id', '=', 'u.id')
|
||||
->join('roles as r', 'r.id', '=', 'ur.role_id')
|
||||
->where('u.school_year', $schoolYear)
|
||||
->where('r.is_active', 1)
|
||||
->whereNull('ur.deleted_at')
|
||||
->groupBy('u.id')
|
||||
@@ -546,23 +540,11 @@ class User extends Authenticatable implements JWTSubject
|
||||
}
|
||||
|
||||
if (! empty($schoolYear)) {
|
||||
// Prefer ur.school_year if it exists (role is year-scoped)
|
||||
$hasUrSchoolYear = false;
|
||||
try {
|
||||
$cols = DB::getSchemaBuilder()->getColumnListing('user_roles');
|
||||
$hasUrSchoolYear = in_array('school_year', $cols, true);
|
||||
} catch (\Throwable $e) {
|
||||
$hasUrSchoolYear = false;
|
||||
}
|
||||
$escYear = DB::getPdo()->quote($schoolYear);
|
||||
|
||||
if ($hasUrSchoolYear) {
|
||||
$q->where('ur.school_year', $schoolYear);
|
||||
} else {
|
||||
$escYear = DB::getPdo()->quote($schoolYear);
|
||||
|
||||
$q->where(function ($w) use ($escYear) {
|
||||
// A) teacher assignment that year
|
||||
$w->whereRaw("
|
||||
$q->where(function ($w) use ($escYear) {
|
||||
// Staff participation in a year comes from yearly relationship tables.
|
||||
$w->whereRaw("
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM teacher_class tc
|
||||
@@ -571,20 +553,15 @@ class User extends Authenticatable implements JWTSubject
|
||||
)
|
||||
");
|
||||
|
||||
// B) OR has any non-teacher-ish, non-parent role
|
||||
$w->orWhereRaw("
|
||||
$w->orWhereRaw("
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM user_roles ur2
|
||||
JOIN roles r2 ON r2.id = ur2.role_id
|
||||
WHERE ur2.user_id = users.id
|
||||
AND ur2.deleted_at IS NULL
|
||||
AND LOWER(r2.name) NOT IN ('parent','ta')
|
||||
AND LOWER(r2.name) NOT LIKE '%teacher%'
|
||||
FROM staff_attendance sa
|
||||
WHERE sa.user_id = users.id
|
||||
AND sa.school_year = {$escYear}
|
||||
)
|
||||
");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$q->groupBy('users.id');
|
||||
|
||||
+2
-10
@@ -18,12 +18,10 @@ class UserRole extends BaseModel
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'role_id',
|
||||
'semester',
|
||||
'school_year',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'updated_by',
|
||||
'deleted_at',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -67,11 +65,7 @@ class UserRole extends BaseModel
|
||||
|
||||
public function scopeForSchoolYear(Builder $q, ?string $schoolYear): Builder
|
||||
{
|
||||
if ($schoolYear === null || trim($schoolYear) === '') {
|
||||
return $q;
|
||||
}
|
||||
|
||||
return $q->where('school_year', $schoolYear);
|
||||
return $q;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -175,8 +169,6 @@ class UserRole extends BaseModel
|
||||
return [
|
||||
'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'],
|
||||
'role_id' => [$req, 'integer', 'min:1', 'exists:roles,id'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'updated_by' => ['nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user