diff --git a/.env.docker.dev b/.env.docker.dev index 698af533..bfa07421 100644 --- a/.env.docker.dev +++ b/.env.docker.dev @@ -1,88 +1,132 @@ -# ──────────────────────────────────── -# Docker Dev Environment -# ──────────────────────────────────── -NODE_ENV=development +APP_NAME=Alrahma_API +APP_ENV=local +APP_KEY=base64:RfIZKqgXC9seghl2Jqs2MgLh1X1Z7APRsnhUK8CgWx8= +APP_DEBUG=true +APP_TIMEZONE=America/New_York +APP_URL=http://localhost:8080 -# ──────────────────────────────────── -# DATABASE (PostgreSQL via docker) -# ──────────────────────────────────── -DATABASE_URL=postgresql://postgres:password@postgres:5432/rentaldrivego +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US -# ──────────────────────────────────── +APP_MAINTENANCE_DRIVER=file + +LOG_CHANNEL=stack +LOG_LEVEL=debug +LOG_DEPRECATIONS_CHANNEL=null + +# ---------------------------- +# DATABASE +# ---------------------------- +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=school_api +DB_USERNAME=root +DB_PASSWORD=8>f398yxL +APP_URL=http://127.0.0.1:8000 + +# ---------------------------- +# SESSION +# ---------------------------- +SESSION_DRIVER=file +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +# ---------------------------- +# CACHE & QUEUE +# ---------------------------- +CACHE_STORE=file +QUEUE_CONNECTION=sync + +# ---------------------------- # REDIS -# ──────────────────────────────────── -REDIS_URL=redis://redis:6379 +# ---------------------------- +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 -# ──────────────────────────────────── +# ---------------------------- +# MAIL +# ---------------------------- +MAIL_MAILER=log +MAIL_PROFILE_DEFAULT=default +MAIL_DEFAULT_HOST=smtp.gmail.com +MAIL_DEFAULT_PORT=587 +MAIL_DEFAULT_USER=alrahma.sunday.school@gmail.com +MAIL_DEFAULT_PASS="psnp emdq dykw ypul" +MAIL_DEFAULT_ENCRYPTION=tls +MAIL_DEFAULT_FROM_EMAIL="alrahma.sunday.school@gmail.com" +MAIL_DEFAULT_FROM_NAME="Alrahma API" +MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com" +MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School" +MAIL_COMMUNICATION_REPLY_TO="alrahma.isgl@gmail.com" +MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications" +MAIL_PAYMENT_REPLY_TO="alrahma.isgl@gmail.com" +MAIL_PAYMENT_REPLY_TO_NAME="School Payments" +MAIL_SENDERS='{"general":{"name":"Alrahma API","email":"alrahma.sunday.school@gmail.com"},"communication":{"name":"School Communications","email":"alrahma.sunday.school@gmail.com"},"payment":{"name":"School Payments","email":"alrahma.sunday.school@gmail.com"}}' + +# Laravel mailer compatibility +MAIL_HOST="${MAIL_DEFAULT_HOST}" +MAIL_PORT="${MAIL_DEFAULT_PORT}" +MAIL_USERNAME="${MAIL_DEFAULT_USER}" +MAIL_PASSWORD="${MAIL_DEFAULT_PASS}" +MAIL_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" +MAIL_FROM_ADDRESS="${MAIL_DEFAULT_FROM_EMAIL}" +MAIL_FROM_NAME="${MAIL_DEFAULT_FROM_NAME}" + +# Legacy fallback keys still read by some services +SMTP_HOST="${MAIL_DEFAULT_HOST}" +SMTP_USER="${MAIL_DEFAULT_USER}" +SMTP_PASS="${MAIL_DEFAULT_PASS}" +SMTP_PORT="${MAIL_DEFAULT_PORT}" +SMTP_ENCRYPTION="${MAIL_DEFAULT_ENCRYPTION}" + +# ---------------------------- +# FILESYSTEM +# ---------------------------- +FILESYSTEM_DISK=local + +# ---------------------------- +# AWS STORAGE +# ---------------------------- +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +# ---------------------------- # JWT -# ──────────────────────────────────── -JWT_SECRET=bvDokCe6RD7Jb5cxJ6P5z4rdTwS37hyUgglD8HGCE6Xwo6g3SEuZ9XNzjou53raUf6B2jMcEWxQm0Y5zcw2THw -JWT_EXPIRY=8h +# ---------------------------- +JWT_SECRET=Uj8rGnYcXMgeRc5qCIn9Wn03tYo1pCsBz1Biou8T9zWtaNxBYi3P4NP5vuFiXHmd +JWT_ALGO=HS256 +JWT_TTL=60 +JWT_REFRESH_TTL=20160 +JWT_BLACKLIST_ENABLED=true +JWT_BLACKLIST_GRACE_PERIOD=0 -# ──────────────────────────────────── -# FILE STORAGE -# ──────────────────────────────────── -FILE_STORAGE_ROOT=/var/lib/rentaldrivego/storage +# ---------------------------- +# DOCS +# ---------------------------- +DOCS_CLIENT_URL=http://localhost:3000 +DOCS_INDEX_TITLE=Alrahma API Documentation +DOCS_INDEX_DESCRIPTION= -# ──────────────────────────────────── -# URLS (docker services) -# ──────────────────────────────────── -API_URL=http://localhost:4000 -API_HOST=0.0.0.0 -API_PORT=4000 -API_INTERNAL_URL=http://api:4000 -ADMIN_URL=http://localhost:3002 -DASHBOARD_URL=http://localhost:3001 -MARKETPLACE_URL=http://localhost:3000 -NEXT_PUBLIC_ADMIN_URL=http://localhost:3002 -NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3001 -NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000 -NEXT_PUBLIC_CUSTOM_DOMAIN_TARGET=http://localhost:3000 +# ---------------------------- +# BADGE +# ---------------------------- +BADGE_SCHOOL_NAME=Al Rahma Sunday School +BADGE_HEADER_SUBTITLE= +BADGE_MOTTO= +BADGE_MOTTO_FALLBACK= +BADGE_ROLE_LABEL=Student +BADGE_FOOTER_ADDRESS=5 Courthouse Lane, Chelmsford, MA 01824, USA -# ──────────────────────────────────── -# CORS -# ──────────────────────────────────── -CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002 - -# ──────────────────────────────────── -# MAIL (log driver in dev) -# ──────────────────────────────────── -MAIL_HOST=mailhog -MAIL_PORT=1025 -MAIL_USERNAME= -MAIL_PASSWORD= -MAIL_ENCRYPTION=none -MAIL_FROM_ADDRESS=dev@rentaldrivego.local -MAIL_FROM_NAME="RentalDriveGo Dev" -MAIL_REPLY_TO_ADDRESS=dev@rentaldrivego.local -MAIL_REPLY_TO_NAME="RentalDriveGo Dev" - -# ──────────────────────────────────── -# ADMIN SEED (initial dev account) -# ──────────────────────────────────── -ADMIN_SEED_EMAIL=admin@rentaldrivego.dev -ADMIN_SEED_FIRST_NAME=Dev -ADMIN_SEED_LAST_NAME=Admin -ADMIN_SEED_PASSWORD=password - -# ──────────────────────────────────── -# EXTERNAL SERVICES (blank in dev) -# ──────────────────────────────────── -AMANPAY_BASE_URL= -AMANPAY_MERCHANT_ID= -AMANPAY_SECRET_KEY= -AMANPAY_WEBHOOK_SECRET= -PAYPAL_BASE_URL= -PAYPAL_CLIENT_ID= -PAYPAL_CLIENT_SECRET= -PAYPAL_WEBHOOK_ID= -TWILIO_ACCOUNT_SID= -TWILIO_AUTH_TOKEN= -TWILIO_PHONE_NUMBER= -TWILIO_WHATSAPP_NUMBER= -FIREBASE_CLIENT_EMAIL= -FIREBASE_PRIVATE_KEY= -FIREBASE_PROJECT_ID= -RESEND_API_KEY= -EMAIL_FROM= -EMAIL_FROM_NAME= +# ---------------------------- +# FRONT-END +# ---------------------------- +VITE_APP_NAME="${APP_NAME}" diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..3883d3c7 --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +VITE_API_URL=https://api.alrahmaisgl.org/api \ No newline at end of file diff --git a/app/.DS_Store b/app/.DS_Store index f418beda..9210c5c3 100644 Binary files a/app/.DS_Store and b/app/.DS_Store differ diff --git a/app/Models/LoginActivity.php b/app/Models/LoginActivity.php index 30e85d7f..34b901cf 100644 --- a/app/Models/LoginActivity.php +++ b/app/Models/LoginActivity.php @@ -6,10 +6,17 @@ class LoginActivity extends BaseModel { protected $table = 'login_activity'; - // ✅ legacy: useTimestamps = true (created_at/updated_at) public $timestamps = true; - protected $fillable = ['user_id', 'email', 'login_time', 'logout_time', 'ip_address', 'user_agent', 'created_at', 'updated_at', 'semester', 'school_year']; + protected $fillable = [ + 'user_id', + 'email', + 'login_time', + 'logout_time', + 'ip_address', + 'user_agent', + 'school_year', + ]; protected $casts = [ 'user_id' => 'integer', @@ -17,15 +24,11 @@ class LoginActivity extends BaseModel 'logout_time' => 'datetime', ]; - /* Optional relationship */ public function user() { return $this->belongsTo(User::class, 'user_id'); } - /** - * Equivalent of legacy getLastActivities() - */ public static function getLastActivities(int $limit = 4) { return static::query() @@ -33,4 +36,4 @@ class LoginActivity extends BaseModel ->limit(max(1, $limit)) ->get(); } -} +} \ No newline at end of file diff --git a/app/Models/Student.php b/app/Models/Student.php index c8c18f9f..defc5c2c 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -132,25 +132,36 @@ class Student extends BaseModel /** * legacy: getNewStudentsWithParentsAndEmergency() - * Picks one emergency contact via MIN(...) with groupBy student. + * Aggregates emergency contacts by parent before joining, avoiding + * duplicate student rows and ONLY_FULL_GROUP_BY violations. */ public static function getNewStudentsWithParentsAndEmergency(): array { + $emergencyContacts = DB::table('emergency_contacts') + ->select([ + 'parent_id', + DB::raw('MIN(emergency_contact_name) AS emergency_name'), + DB::raw('MIN(relation) AS emergency_relationship'), + DB::raw('MIN(cellphone) AS emergency_phone'), + ]) + ->groupBy('parent_id'); + return DB::table('students') - ->selectRaw(' - students.*, - u.firstname AS parent_firstname, - u.lastname AS parent_lastname, - u.email AS parent_email, - u.cellphone AS parent_phone, - MIN(ec.emergency_contact_name) AS emergency_name, - MIN(ec.relation) AS emergency_relationship, - MIN(ec.cellphone) AS emergency_phone - ') ->leftJoin('users as u', 'u.id', '=', 'students.parent_id') - ->leftJoin('emergency_contacts as ec', 'ec.parent_id', '=', 'students.parent_id') + ->leftJoinSub($emergencyContacts, 'ec', function ($join): void { + $join->on('ec.parent_id', '=', 'students.parent_id'); + }) + ->select([ + 'students.*', + 'u.firstname as parent_firstname', + 'u.lastname as parent_lastname', + 'u.email as parent_email', + 'u.cellphone as parent_phone', + 'ec.emergency_name', + 'ec.emergency_relationship', + 'ec.emergency_phone', + ]) ->where('students.is_new', 1) - ->groupBy('students.id') ->orderBy('students.lastname', 'asc') ->orderBy('students.firstname', 'asc') ->get() @@ -163,46 +174,65 @@ class Student extends BaseModel */ public static function getStudentsWithParentsAndEmergency(?string $schoolYear = null, ?int $isNew = null): array { - $q = DB::table('students') - ->selectRaw(' - students.*, - u.firstname AS parent_firstname, - u.lastname AS parent_lastname, - u.email AS parent_email, - u.cellphone AS parent_phone, - MIN(ec.emergency_contact_name) AS emergency_name, - MIN(ec.relation) AS emergency_relationship, - MIN(ec.cellphone) AS emergency_phone - ') - ->leftJoin('users as u', 'u.id', '=', 'students.parent_id') - ->leftJoin('emergency_contacts as ec', 'ec.parent_id', '=', 'students.parent_id'); + $emergencyContacts = DB::table('emergency_contacts') + ->select([ + 'parent_id', + DB::raw('MIN(emergency_contact_name) AS emergency_name'), + DB::raw('MIN(relation) AS emergency_relationship'), + DB::raw('MIN(cellphone) AS emergency_phone'), + ]) + ->groupBy('parent_id'); + + $q = DB::table('students') + ->leftJoin('users as u', 'u.id', '=', 'students.parent_id') + ->leftJoinSub($emergencyContacts, 'ec', function ($join): void { + $join->on('ec.parent_id', '=', 'students.parent_id'); + }) + ->select([ + 'students.*', + 'u.firstname as parent_firstname', + 'u.lastname as parent_lastname', + 'u.email as parent_email', + 'u.cellphone as parent_phone', + 'ec.emergency_name', + 'ec.emergency_relationship', + 'ec.emergency_phone', + ]); - // is_new filter if ($isNew === 0 || $isNew === 1) { $q->where('students.is_new', $isNew); } else { $q->whereIn('students.is_new', [0, 1]); } - // Students are global identities; year rosters come from yearly class/enrollment rows. - if ($schoolYear !== null && strtolower($schoolYear) !== 'all') { - $q->where(function ($yearQuery) use ($schoolYear) { - $yearQuery->whereExists(function ($sub) use ($schoolYear) { - $sub->selectRaw('1') - ->from('student_class as sc') - ->whereColumn('sc.student_id', 'students.id') - ->where('sc.school_year', $schoolYear); - })->orWhereExists(function ($sub) use ($schoolYear) { - $sub->selectRaw('1') - ->from('enrollments as e') - ->whereColumn('e.student_id', 'students.id') - ->where('e.school_year', $schoolYear); - }); + $normalizedSchoolYear = $schoolYear !== null + ? trim($schoolYear) + : null; + + if ( + $normalizedSchoolYear !== null + && $normalizedSchoolYear !== '' + && strtolower($normalizedSchoolYear) !== 'all' + ) { + $q->where(function ($yearQuery) use ($normalizedSchoolYear): void { + $yearQuery + ->whereExists(function ($sub) use ($normalizedSchoolYear): void { + $sub->selectRaw('1') + ->from('student_class as sc') + ->whereColumn('sc.student_id', 'students.id') + ->where('sc.school_year', $normalizedSchoolYear); + }) + ->orWhereExists(function ($sub) use ($normalizedSchoolYear): void { + $sub->selectRaw('1') + ->from('enrollments as e') + ->whereColumn('e.student_id', 'students.id') + ->where('e.school_year', $normalizedSchoolYear); + }); }); } - return $q->groupBy('students.id') - ->orderByDesc('students.is_new') // new first + return $q + ->orderByDesc('students.is_new') ->orderBy('students.lastname', 'asc') ->orderBy('students.firstname', 'asc') ->get() @@ -403,4 +433,4 @@ class Student extends BaseModel 'is_active' => ['nullable', 'boolean'], ]; } -} +} \ No newline at end of file diff --git a/app/Services/Auth/ApiLoginSecurityService.php b/app/Services/Auth/ApiLoginSecurityService.php index 914b64a6..0defa00f 100644 --- a/app/Services/Auth/ApiLoginSecurityService.php +++ b/app/Services/Auth/ApiLoginSecurityService.php @@ -22,7 +22,10 @@ class ApiLoginSecurityService public function isIpBlocked(string $ip): bool { - $row = IpAttempt::query()->where('ip_address', $ip)->first(); + $row = IpAttempt::query() + ->where('ip_address', $ip) + ->first(); + if (! $row || ! $row->blocked_until) { return false; } @@ -35,14 +38,16 @@ class ApiLoginSecurityService $now = CarbonImmutable::now('UTC'); $nowStr = $now->toDateTimeString(); - $attempt = IpAttempt::query()->where('ip_address', $ip)->first(); + $attempt = IpAttempt::query() + ->where('ip_address', $ip) + ->first(); + if ($attempt) { $previouslyBlocked = $attempt->blocked_until ? CarbonImmutable::parse($attempt->blocked_until, 'UTC')->isFuture() : false; - // If the previous block has already expired, start a fresh window - // so we don't perma-ban an IP after its first 10 lifetime failures. + // If the previous block has expired, begin a fresh failure window. $attempts = $previouslyBlocked ? ((int) $attempt->attempts) + 1 : 1; @@ -64,14 +69,19 @@ class ApiLoginSecurityService 'ip_address' => $ip, 'attempts' => 1, 'last_attempt_at' => $nowStr, + 'blocked_until' => null, ]); } - public function handleFailedLogin(User $user, string $email, string $ip): void - { + public function handleFailedLogin( + User $user, + string $email, + string $ip + ): void { $user->refresh(); $failedAttempts = ((int) ($user->failed_attempts ?? 0)) + 1; + $data = [ 'failed_attempts' => $failedAttempts, 'last_failed_at' => utc_now(), @@ -79,8 +89,14 @@ class ApiLoginSecurityService if ($failedAttempts >= 3) { $data['is_suspended'] = true; - // Password-reset delivery can be wired here when the suspension workflow is finalized. - Log::notice('api_login: account suspended after failed attempts', ['email' => $email, 'user_id' => $user->id]); + + Log::notice( + 'api_login: account suspended after failed attempts', + [ + 'email' => $email, + 'user_id' => $user->id, + ] + ); } $user->fill($data); @@ -96,10 +112,16 @@ class ApiLoginSecurityService $user->save(); } - public function logSuccessfulLogin(User $user, Request $request): void - { - $schoolYear = trim((string) (Configuration::getConfig('school_year') ?? date('Y'))); - $semester = trim((string) (Configuration::getConfig('semester') ?? 'Fall')); + public function logSuccessfulLogin( + User $user, + Request $request + ): void { + $schoolYear = trim( + (string) ( + Configuration::getConfig('school_year') + ?? date('Y') + ) + ); LoginActivity::query()->create([ 'user_id' => $user->id, @@ -107,21 +129,24 @@ class ApiLoginSecurityService 'login_time' => utc_now(), 'ip_address' => $request->ip(), 'user_agent' => (string) ($request->userAgent() ?? ''), - 'school_year' => $schoolYear, - 'semester' => $semester !== '' ? $semester : 'Fall', + 'school_year' => $schoolYear !== '' + ? $schoolYear + : date('Y'), ]); } /** - * Returns roles as an object map {"RoleName": true}. + * Returns roles as an object map: + * {"RoleName": true} * - * @param list $roleNames + * @param list $roleNames */ public function rolesToObjectMap(array $roleNames): object { $rolesMap = []; - foreach ($roleNames as $r) { - $rolesMap[$r] = true; + + foreach ($roleNames as $roleName) { + $rolesMap[$roleName] = true; } return (object) $rolesMap; @@ -130,19 +155,40 @@ class ApiLoginSecurityService /** * Top-level JSON body for API login success responses. * - * @return array{status: true, token: string, access_token: string, token_type: string, expires_in: int, user: array{id: int, name: string, firstname: ?string, lastname: ?string, email: ?string, roles: object, class_section_id: ?int, class_section_name: ?string}} + * @return array{ + * status: true, + * token: string, + * access_token: string, + * token_type: string, + * expires_in: int, + * user: array{ + * id: int, + * name: string, + * firstname: ?string, + * lastname: ?string, + * email: ?string, + * roles: object, + * class_section_id: ?int, + * class_section_name: ?string + * } + * } */ - public function buildLoginResponse(User $user, string $jwtToken): array - { + public function buildLoginResponse( + User $user, + string $jwtToken + ): array { $roleNames = $user->roleNames(); - $fullName = trim(($user->firstname ?? '').' '.($user->lastname ?? '')); + + $fullName = trim( + ($user->firstname ?? '') + .' '. + ($user->lastname ?? '') + ); + $teacherContext = $user->teacherSessionContext(); return [ 'status' => true, - // Keep both names. Older clients read `token`; JWT-aware clients often read - // `access_token`. Removing either one just makes login fail in a new and - // irritatingly avoidable way. 'token' => $jwtToken, 'access_token' => $jwtToken, 'token_type' => 'bearer', @@ -170,4 +216,4 @@ class ApiLoginSecurityService return (int) ($ttl ?? 60) * 60; } -} +} \ No newline at end of file diff --git a/app/Services/Families/ParentProfileAdminQueryService.php b/app/Services/Families/ParentProfileAdminQueryService.php index 3b332359..e8cec4f9 100644 --- a/app/Services/Families/ParentProfileAdminQueryService.php +++ b/app/Services/Families/ParentProfileAdminQueryService.php @@ -4,7 +4,9 @@ namespace App\Services\Families; use App\Models\StudentClass; use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class ParentProfileAdminQueryService { @@ -12,7 +14,53 @@ class ParentProfileAdminQueryService public function index(string $schoolYear, string $search = '', int $perPage = 25): LengthAwarePaginator { + $familyCounts = DB::table('family_guardians') + ->select('user_id', DB::raw('COUNT(DISTINCT family_id) AS families_count')) + ->groupBy('user_id'); + + $familyStudentMap = DB::table('family_guardians as pfg') + ->join('family_students as pfs', 'pfs.family_id', '=', 'pfg.family_id') + ->joinSub($this->schoolYearStudentIds($schoolYear), 'pys', function ($join): void { + $join->on('pys.student_id', '=', 'pfs.student_id'); + }) + ->selectRaw('pfg.user_id AS parent_id, pfs.student_id AS student_id'); + + $directStudentMap = DB::table('students as ps') + ->joinSub($this->schoolYearStudentIds($schoolYear), 'dys', function ($join): void { + $join->on('dys.student_id', '=', 'ps.id'); + }) + ->whereNotNull('ps.parent_id') + ->selectRaw('ps.parent_id AS parent_id, ps.id AS student_id'); + + $parentStudentCounts = DB::query() + ->fromSub( + $familyStudentMap->union($directStudentMap), + 'parent_students' + ) + ->select( + 'parent_id', + DB::raw('COUNT(DISTINCT student_id) AS selected_year_students_count') + ) + ->groupBy('parent_id'); + + $invoiceBalances = DB::table('invoices') + ->select('parent_id', DB::raw('COALESCE(SUM(balance), 0) AS balance')) + ->whereNotNull('parent_id'); + + $this->applySchoolYearFilter($invoiceBalances, 'school_year', $schoolYear); + + $invoiceBalances->groupBy('parent_id'); + $query = DB::table('users as u') + ->leftJoinSub($familyCounts, 'fc', function ($join): void { + $join->on('fc.user_id', '=', 'u.id'); + }) + ->leftJoinSub($parentStudentCounts, 'psc', function ($join): void { + $join->on('psc.parent_id', '=', 'u.id'); + }) + ->leftJoinSub($invoiceBalances, 'ib', function ($join): void { + $join->on('ib.parent_id', '=', 'u.id'); + }) ->select( 'u.id', 'u.firstname', @@ -20,40 +68,52 @@ class ParentProfileAdminQueryService 'u.email', 'u.cellphone', 'u.status', - DB::raw('COUNT(DISTINCT fg.family_id) as families_count'), - DB::raw('COUNT(DISTINCT s.id) as selected_year_students_count'), - DB::raw('COALESCE(SUM(DISTINCT i.balance), 0) as balance') + DB::raw('COALESCE(fc.families_count, 0) AS families_count'), + DB::raw('COALESCE(psc.selected_year_students_count, 0) AS selected_year_students_count'), + DB::raw('COALESCE(ib.balance, 0) AS balance') ) - ->leftJoin('family_guardians as fg', 'fg.user_id', '=', 'u.id') - ->leftJoin('family_students as fs', 'fs.family_id', '=', 'fg.family_id') - ->leftJoin('students as s', function ($join) use ($schoolYear) { - $join->on('s.id', '=', 'fs.student_id') - ->where('s.school_year', '=', $schoolYear); - }) - ->leftJoin('invoices as i', function ($join) use ($schoolYear) { - $join->on('i.parent_id', '=', 'u.id') - ->where('i.school_year', '=', $schoolYear); - }) ->where($this->relevantParentConstraint($schoolYear)) - ->groupBy('u.id', 'u.firstname', 'u.lastname', 'u.email', 'u.cellphone', 'u.status') ->orderBy('u.lastname') ->orderBy('u.firstname'); + $search = trim($search); + if ($search !== '') { $needle = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $search).'%'; - $query->where(function ($inner) use ($needle) { - $inner->whereRaw("CONCAT_WS(' ', u.firstname, u.lastname) LIKE ?", [$needle]) + + $query->where(function ($inner) use ($needle, $schoolYear): void { + $inner + ->whereRaw("CONCAT_WS(' ', u.firstname, u.lastname) LIKE ?", [$needle]) ->orWhere('u.email', 'like', $needle) ->orWhere('u.cellphone', 'like', $needle) - ->orWhereExists(function ($exists) use ($needle) { + ->orWhereExists(function ($exists) use ($needle, $schoolYear): void { $exists->selectRaw('1') ->from('family_guardians as sfg') ->join('family_students as sfs', 'sfs.family_id', '=', 'sfg.family_id') ->join('students as ss', 'ss.id', '=', 'sfs.student_id') ->whereColumn('sfg.user_id', 'u.id') - ->where(function ($student) use ($needle) { - $student->whereRaw("CONCAT_WS(' ', ss.firstname, ss.lastname) LIKE ?", [$needle]); - }); + ->whereRaw("CONCAT_WS(' ', ss.firstname, ss.lastname) LIKE ?", [$needle]); + + $this->constrainStudentToSchoolYear( + $exists, + 'ss.id', + $schoolYear + ); + }) + ->orWhereExists(function ($exists) use ($needle, $schoolYear): void { + $exists->selectRaw('1') + ->from('students as direct_search_students') + ->whereColumn('direct_search_students.parent_id', 'u.id') + ->whereRaw( + "CONCAT_WS(' ', direct_search_students.firstname, direct_search_students.lastname) LIKE ?", + [$needle] + ); + + $this->constrainStudentToSchoolYear( + $exists, + 'direct_search_students.id', + $schoolYear + ); }); }); } @@ -76,7 +136,18 @@ class ParentProfileAdminQueryService public function show(int $parentId, string $schoolYear): ?array { $parent = DB::table('users') - ->select('id', 'firstname', 'lastname', 'email', 'cellphone', 'address_street', 'city', 'state', 'zip', 'status') + ->select( + 'id', + 'firstname', + 'lastname', + 'email', + 'cellphone', + 'address_street', + 'city', + 'state', + 'zip', + 'status' + ) ->where('id', $parentId) ->first(); @@ -91,13 +162,14 @@ class ParentProfileAdminQueryService 'lastname' => $parent->lastname, ], ]; + $finance = $this->finance->loadFinancialsForParents([$parentId], $schoolYear); return [ 'parent' => (array) $parent, 'families' => $this->familiesForParent($parentId, $schoolYear), 'students' => $this->studentsForParent($parentId, $schoolYear), - 'emergency_contacts' => $this->emergencyContactsForParents($guardians, $schoolYear), + 'emergency_contacts' => $this->emergencyContactsForParents($guardians), 'invoices' => $finance['invoices'], 'payments' => $finance['payments'], 'finance_summary' => $finance['summary'], @@ -131,6 +203,7 @@ class ParentProfileAdminQueryService public function parentSummary(array $row, string $schoolYear): array { $balance = (float) ($row['balance'] ?? 0); + $primaryFamily = DB::table('family_guardians as fg') ->join('families as f', 'f.id', '=', 'fg.family_id') ->where('fg.user_id', (int) ($row['id'] ?? 0)) @@ -159,35 +232,66 @@ class ParentProfileAdminQueryService private function relevantParentConstraint(string $schoolYear): callable { - return function ($query) use ($schoolYear) { - $query->whereExists(function ($exists) use ($schoolYear) { - $exists->selectRaw('1') - ->from('family_guardians as rfg') - ->join('family_students as rfs', 'rfs.family_id', '=', 'rfg.family_id') - ->join('students as rs', 'rs.id', '=', 'rfs.student_id') - ->whereColumn('rfg.user_id', 'u.id') - ->where('rs.school_year', $schoolYear); - })->orWhereExists(function ($exists) use ($schoolYear) { - $exists->selectRaw('1') - ->from('students as direct_students') - ->whereColumn('direct_students.parent_id', 'u.id') - ->where('direct_students.school_year', $schoolYear); - })->orWhereExists(function ($exists) use ($schoolYear) { - $exists->selectRaw('1') - ->from('invoices as ri') - ->whereColumn('ri.parent_id', 'u.id') - ->where('ri.school_year', $schoolYear); - })->orWhereExists(function ($exists) use ($schoolYear) { - $exists->selectRaw('1') - ->from('parent_accounts as pa') - ->whereColumn('pa.parent_id', 'u.id') - ->where('pa.school_year', $schoolYear); - })->orWhereExists(function ($exists) use ($schoolYear) { - $exists->selectRaw('1') - ->from('emergency_contacts as ec') - ->whereColumn('ec.parent_id', 'u.id') - ->where('ec.school_year', $schoolYear); - }); + $schoolYearId = $this->schoolYearId($schoolYear); + + return function ($query) use ($schoolYear, $schoolYearId): void { + $query + ->whereExists(function ($exists) use ($schoolYear): void { + $exists->selectRaw('1') + ->from('family_guardians as rfg') + ->join('family_students as rfs', 'rfs.family_id', '=', 'rfg.family_id') + ->whereColumn('rfg.user_id', 'u.id'); + + $this->constrainStudentToSchoolYear( + $exists, + 'rfs.student_id', + $schoolYear + ); + }) + ->orWhereExists(function ($exists) use ($schoolYear): void { + $exists->selectRaw('1') + ->from('students as direct_students') + ->whereColumn('direct_students.parent_id', 'u.id'); + + $this->constrainStudentToSchoolYear( + $exists, + 'direct_students.id', + $schoolYear + ); + }) + ->orWhereExists(function ($exists) use ($schoolYear): void { + $exists->selectRaw('1') + ->from('invoices as ri') + ->whereColumn('ri.parent_id', 'u.id'); + + $this->applySchoolYearFilter( + $exists, + 'ri.school_year', + $schoolYear + ); + }); + + if (! Schema::hasTable('parent_accounts')) { + return; + } + + if ($this->isAllSchoolYears($schoolYear)) { + $query->orWhereExists(function ($exists): void { + $exists->selectRaw('1') + ->from('parent_accounts as pa') + ->whereColumn('pa.parent_id', 'u.id'); + }); + } elseif ( + $schoolYearId !== null + && Schema::hasColumn('parent_accounts', 'school_year_id') + ) { + $query->orWhereExists(function ($exists) use ($schoolYearId): void { + $exists->selectRaw('1') + ->from('parent_accounts as pa') + ->whereColumn('pa.parent_id', 'u.id') + ->where('pa.school_year_id', $schoolYearId); + }); + } }; } @@ -196,15 +300,25 @@ class ParentProfileAdminQueryService return DB::table('family_guardians as fg') ->join('families as f', 'f.id', '=', 'fg.family_id') ->where('fg.user_id', $parentId) - ->whereExists(function ($exists) use ($schoolYear) { + ->whereExists(function ($exists) use ($schoolYear): void { $exists->selectRaw('1') ->from('family_students as fs') - ->join('students as s', 's.id', '=', 'fs.student_id') - ->whereColumn('fs.family_id', 'f.id') - ->where('s.school_year', $schoolYear); + ->whereColumn('fs.family_id', 'f.id'); + + $this->constrainStudentToSchoolYear( + $exists, + 'fs.student_id', + $schoolYear + ); }) ->orderBy('f.household_name') - ->select('f.*', 'fg.relation', 'fg.is_primary', 'fg.receive_emails', 'fg.receive_sms') + ->select( + 'f.*', + 'fg.relation', + 'fg.is_primary', + 'fg.receive_emails', + 'fg.receive_sms' + ) ->get() ->map(fn ($row) => (array) $row) ->all(); @@ -212,19 +326,27 @@ class ParentProfileAdminQueryService private function studentsForParent(int $parentId, string $schoolYear): array { - $rows = DB::table('students as s') + $rowsQuery = DB::table('students as s') ->select('s.id', 's.firstname', 's.lastname', 's.parent_id') - ->where('s.school_year', $schoolYear) - ->where(function ($query) use ($parentId) { - $query->where('s.parent_id', $parentId) - ->orWhereExists(function ($exists) use ($parentId) { + ->where(function ($query) use ($parentId): void { + $query + ->where('s.parent_id', $parentId) + ->orWhereExists(function ($exists) use ($parentId): void { $exists->selectRaw('1') ->from('family_guardians as fg') ->join('family_students as fs', 'fs.family_id', '=', 'fg.family_id') ->whereColumn('fs.student_id', 's.id') ->where('fg.user_id', $parentId); }); - }) + }); + + $this->constrainStudentToSchoolYear( + $rowsQuery, + 's.id', + $schoolYear + ); + + $rows = $rowsQuery ->orderBy('s.lastname') ->orderBy('s.firstname') ->get() @@ -233,9 +355,11 @@ class ParentProfileAdminQueryService foreach ($rows as &$row) { $studentId = (int) ($row['id'] ?? 0); + $classSectionName = $studentId > 0 ? (string) (StudentClass::getClassSectionsByStudentId($studentId, $schoolYear) ?? '') : ''; + $row['class_section_name'] = $classSectionName; $row['grade'] = $classSectionName; } @@ -244,10 +368,10 @@ class ParentProfileAdminQueryService return $rows; } - private function emergencyContactsForParents(array $guardians, string $schoolYear): array + private function emergencyContactsForParents(array $guardians): array { $parentIds = array_values(array_filter(array_map( - static fn ($g) => (int) ($g['user_id'] ?? 0), + static fn ($guardian) => (int) ($guardian['user_id'] ?? 0), $guardians ))); @@ -256,12 +380,105 @@ class ParentProfileAdminQueryService } return DB::table('emergency_contacts') - ->select('id', 'parent_id', 'emergency_contact_name', 'relation', 'cellphone', 'email', 'school_year', 'semester', 'created_at', 'updated_at') + ->select( + 'id', + 'parent_id', + 'emergency_contact_name', + 'relation', + 'cellphone', + 'email', + 'created_at', + 'updated_at' + ) ->whereIn('parent_id', $parentIds) - ->where('school_year', $schoolYear) ->orderByDesc('updated_at') ->get() ->map(fn ($row) => (array) $row) ->all(); } -} + + private function schoolYearStudentIds(string $schoolYear): QueryBuilder + { + $studentClass = DB::table('student_class') + ->select('student_id'); + + $enrollments = DB::table('enrollments') + ->select('student_id'); + + if (! $this->isAllSchoolYears($schoolYear)) { + $studentClass->where('school_year', $schoolYear); + $enrollments->where('school_year', $schoolYear); + } + + return $studentClass->union($enrollments); + } + + private function constrainStudentToSchoolYear( + QueryBuilder $query, + string $studentIdColumn, + string $schoolYear + ): void { + if ($this->isAllSchoolYears($schoolYear)) { + return; + } + + $query->where(function ($yearQuery) use ($studentIdColumn, $schoolYear): void { + $yearQuery + ->whereExists(function ($exists) use ($studentIdColumn, $schoolYear): void { + $exists->selectRaw('1') + ->from('student_class as year_sc') + ->whereColumn('year_sc.student_id', $studentIdColumn) + ->where('year_sc.school_year', $schoolYear); + }) + ->orWhereExists(function ($exists) use ($studentIdColumn, $schoolYear): void { + $exists->selectRaw('1') + ->from('enrollments as year_e') + ->whereColumn('year_e.student_id', $studentIdColumn) + ->where('year_e.school_year', $schoolYear); + }); + }); + } + + + private function schoolYearId(string $schoolYear): ?int + { + if ($this->isAllSchoolYears($schoolYear) || ! Schema::hasTable('school_years')) { + return null; + } + + $schoolYear = trim($schoolYear); + + foreach (['school_year', 'name', 'year', 'label'] as $column) { + if (! Schema::hasColumn('school_years', $column)) { + continue; + } + + $id = DB::table('school_years') + ->where($column, $schoolYear) + ->value('id'); + + if ($id !== null) { + return (int) $id; + } + } + + return null; + } + + private function applySchoolYearFilter( + QueryBuilder $query, + string $column, + string $schoolYear + ): void { + if (! $this->isAllSchoolYears($schoolYear)) { + $query->where($column, $schoolYear); + } + } + + private function isAllSchoolYears(string $schoolYear): bool + { + $schoolYear = trim($schoolYear); + + return $schoolYear === '' || strtolower($schoolYear) === 'all'; + } +} \ No newline at end of file diff --git a/bootstrap/cache/packages.php b/bootstrap/cache/packages.php deleted file mode 100755 index 9fe4c742..00000000 --- a/bootstrap/cache/packages.php +++ /dev/null @@ -1,56 +0,0 @@ - - array ( - 'providers' => - array ( - 0 => 'Laravel\\Pail\\PailServiceProvider', - ), - ), - 'laravel/sail' => - array ( - 'providers' => - array ( - 0 => 'Laravel\\Sail\\SailServiceProvider', - ), - ), - 'laravel/sanctum' => - array ( - 'providers' => - array ( - 0 => 'Laravel\\Sanctum\\SanctumServiceProvider', - ), - ), - 'nesbot/carbon' => - array ( - 'providers' => - array ( - 0 => 'Carbon\\Laravel\\ServiceProvider', - ), - ), - 'nunomaduro/collision' => - array ( - 'providers' => - array ( - 0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', - ), - ), - 'nunomaduro/termwind' => - array ( - 'providers' => - array ( - 0 => 'Termwind\\Laravel\\TermwindServiceProvider', - ), - ), - 'php-open-source-saver/jwt-auth' => - array ( - 'aliases' => - array ( - 'JWTAuth' => 'PHPOpenSourceSaver\\JWTAuth\\Facades\\JWTAuth', - 'JWTFactory' => 'PHPOpenSourceSaver\\JWTAuth\\Facades\\JWTFactory', - ), - 'providers' => - array ( - 0 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider', - ), - ), -); \ No newline at end of file diff --git a/bootstrap/cache/services.php b/bootstrap/cache/services.php deleted file mode 100755 index ea5fd7ff..00000000 --- a/bootstrap/cache/services.php +++ /dev/null @@ -1,264 +0,0 @@ - - array ( - 0 => 'Illuminate\\Auth\\AuthServiceProvider', - 1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', - 2 => 'Illuminate\\Bus\\BusServiceProvider', - 3 => 'Illuminate\\Cache\\CacheServiceProvider', - 4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider', - 6 => 'Illuminate\\Cookie\\CookieServiceProvider', - 7 => 'Illuminate\\Database\\DatabaseServiceProvider', - 8 => 'Illuminate\\Encryption\\EncryptionServiceProvider', - 9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', - 10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', - 11 => 'Illuminate\\Hashing\\HashServiceProvider', - 12 => 'Illuminate\\Mail\\MailServiceProvider', - 13 => 'Illuminate\\Notifications\\NotificationServiceProvider', - 14 => 'Illuminate\\Pagination\\PaginationServiceProvider', - 15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', - 16 => 'Illuminate\\Pipeline\\PipelineServiceProvider', - 17 => 'Illuminate\\Queue\\QueueServiceProvider', - 18 => 'Illuminate\\Redis\\RedisServiceProvider', - 19 => 'Illuminate\\Session\\SessionServiceProvider', - 20 => 'Illuminate\\Translation\\TranslationServiceProvider', - 21 => 'Illuminate\\Validation\\ValidationServiceProvider', - 22 => 'Illuminate\\View\\ViewServiceProvider', - 23 => 'Laravel\\Pail\\PailServiceProvider', - 24 => 'Laravel\\Sail\\SailServiceProvider', - 25 => 'Laravel\\Sanctum\\SanctumServiceProvider', - 26 => 'Carbon\\Laravel\\ServiceProvider', - 27 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', - 28 => 'Termwind\\Laravel\\TermwindServiceProvider', - 29 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider', - 30 => 'App\\Providers\\AppServiceProvider', - 31 => 'App\\Providers\\EventServiceProvider', - 32 => 'Laravel\\Sanctum\\SanctumServiceProvider', - ), - 'eager' => - array ( - 0 => 'Illuminate\\Auth\\AuthServiceProvider', - 1 => 'Illuminate\\Cookie\\CookieServiceProvider', - 2 => 'Illuminate\\Database\\DatabaseServiceProvider', - 3 => 'Illuminate\\Encryption\\EncryptionServiceProvider', - 4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', - 5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', - 6 => 'Illuminate\\Notifications\\NotificationServiceProvider', - 7 => 'Illuminate\\Pagination\\PaginationServiceProvider', - 8 => 'Illuminate\\Session\\SessionServiceProvider', - 9 => 'Illuminate\\View\\ViewServiceProvider', - 10 => 'Laravel\\Pail\\PailServiceProvider', - 11 => 'Laravel\\Sanctum\\SanctumServiceProvider', - 12 => 'Carbon\\Laravel\\ServiceProvider', - 13 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider', - 14 => 'Termwind\\Laravel\\TermwindServiceProvider', - 15 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider', - 16 => 'App\\Providers\\AppServiceProvider', - 17 => 'App\\Providers\\EventServiceProvider', - 18 => 'Laravel\\Sanctum\\SanctumServiceProvider', - ), - 'deferred' => - array ( - 'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', - 'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', - 'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', - 'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider', - 'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider', - 'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider', - 'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider', - 'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider', - 'cache' => 'Illuminate\\Cache\\CacheServiceProvider', - 'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider', - 'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider', - 'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider', - 'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider', - 'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', - 'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider', - 'hash' => 'Illuminate\\Hashing\\HashServiceProvider', - 'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider', - 'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider', - 'mailer' => 'Illuminate\\Mail\\MailServiceProvider', - 'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider', - 'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', - 'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', - 'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider', - 'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider', - 'queue' => 'Illuminate\\Queue\\QueueServiceProvider', - 'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider', - 'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider', - 'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider', - 'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider', - 'redis' => 'Illuminate\\Redis\\RedisServiceProvider', - 'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider', - 'translator' => 'Illuminate\\Translation\\TranslationServiceProvider', - 'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider', - 'validator' => 'Illuminate\\Validation\\ValidationServiceProvider', - 'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider', - 'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider', - 'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider', - 'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider', - ), - 'when' => - array ( - 'Illuminate\\Broadcasting\\BroadcastServiceProvider' => - array ( - ), - 'Illuminate\\Bus\\BusServiceProvider' => - array ( - ), - 'Illuminate\\Cache\\CacheServiceProvider' => - array ( - ), - 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' => - array ( - ), - 'Illuminate\\Concurrency\\ConcurrencyServiceProvider' => - array ( - ), - 'Illuminate\\Hashing\\HashServiceProvider' => - array ( - ), - 'Illuminate\\Mail\\MailServiceProvider' => - array ( - ), - 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' => - array ( - ), - 'Illuminate\\Pipeline\\PipelineServiceProvider' => - array ( - ), - 'Illuminate\\Queue\\QueueServiceProvider' => - array ( - ), - 'Illuminate\\Redis\\RedisServiceProvider' => - array ( - ), - 'Illuminate\\Translation\\TranslationServiceProvider' => - array ( - ), - 'Illuminate\\Validation\\ValidationServiceProvider' => - array ( - ), - 'Laravel\\Sail\\SailServiceProvider' => - array ( - ), - ), -); \ No newline at end of file diff --git a/database/migrations/2026_07_05_000001_add_school_year_closure_foundation.php b/database/migrations/2026_07_05_000001_add_school_year_closure_foundation.php index 31e78c11..aec702af 100644 --- a/database/migrations/2026_07_05_000001_add_school_year_closure_foundation.php +++ b/database/migrations/2026_07_05_000001_add_school_year_closure_foundation.php @@ -29,12 +29,15 @@ return new class extends Migration if (! Schema::hasColumn('school_years', 'reopened_at')) { $table->timestamp('reopened_at')->nullable()->after('closed_by'); } + if (! Schema::hasColumn('school_years', 'reopened_by')) { $table->unsignedBigInteger('reopened_by')->nullable()->after('reopened_at'); } + if (! Schema::hasColumn('school_years', 'archived_at')) { $table->timestamp('archived_at')->nullable()->after('reopened_by'); } + if (! Schema::hasColumn('school_years', 'archived_by')) { $table->unsignedBigInteger('archived_by')->nullable()->after('archived_at'); } @@ -51,9 +54,21 @@ return new class extends Migration $table->string('status', 30)->default('enrolled')->index(); $table->unsignedBigInteger('promoted_from_enrollment_id')->nullable(); $table->timestamps(); - $table->unique(['student_id', 'school_year_id'], 'uniq_student_year'); - $table->index('school_year_id', 'idx_enrollment_school_year'); - $table->index('student_id', 'idx_enrollment_student'); + + $table->unique( + ['student_id', 'school_year_id'], + 'uniq_student_year' + ); + + $table->index( + 'school_year_id', + 'idx_enrollment_school_year' + ); + + $table->index( + 'student_id', + 'idx_enrollment_student' + ); }); } @@ -66,14 +81,27 @@ return new class extends Migration $table->decimal('opening_balance', 12, 2)->default(0); $table->decimal('current_balance', 12, 2)->default(0); $table->timestamps(); - $table->unique(['parent_id', 'school_year'], 'uniq_parent_school_year_name'); - $table->index('parent_id', 'idx_parent_account_parent'); - $table->index('school_year_id', 'idx_parent_account_school_year_id'); + + $table->unique( + ['parent_id', 'school_year'], + 'uniq_parent_school_year_name' + ); + + $table->index( + 'parent_id', + 'idx_parent_account_parent' + ); + + $table->index( + 'school_year_id', + 'idx_parent_account_school_year_id' + ); }); } else { Schema::table('parent_accounts', function (Blueprint $table): void { if (! Schema::hasColumn('parent_accounts', 'school_year_id')) { - $table->unsignedBigInteger('school_year_id')->nullable()->after('school_year'); + $table->unsignedBigInteger('school_year_id') + ->nullable(); } }); } @@ -97,31 +125,60 @@ return new class extends Migration $table->text('reversal_reason')->nullable(); $table->unsignedBigInteger('created_by')->nullable(); $table->timestamps(); - $table->unique(['parent_id', 'from_school_year', 'to_school_year'], 'uniq_parent_year_transfer_name'); - $table->index('parent_id', 'idx_balance_transfer_parent'); - $table->index('from_school_year_id', 'idx_balance_transfer_from_year_id'); - $table->index('to_school_year_id', 'idx_balance_transfer_to_year_id'); + + $table->unique( + ['parent_id', 'from_school_year', 'to_school_year'], + 'uniq_parent_year_transfer_name' + ); + + $table->index( + 'parent_id', + 'idx_balance_transfer_parent' + ); + + $table->index( + 'from_school_year_id', + 'idx_balance_transfer_from_year_id' + ); + + $table->index( + 'to_school_year_id', + 'idx_balance_transfer_to_year_id' + ); }); } else { Schema::table('parent_balance_transfers', function (Blueprint $table): void { if (! Schema::hasColumn('parent_balance_transfers', 'from_school_year_id')) { - $table->unsignedBigInteger('from_school_year_id')->nullable()->after('to_school_year'); + $table->unsignedBigInteger('from_school_year_id') + ->nullable(); } + if (! Schema::hasColumn('parent_balance_transfers', 'to_school_year_id')) { - $table->unsignedBigInteger('to_school_year_id')->nullable()->after('from_school_year_id'); + $table->unsignedBigInteger('to_school_year_id') + ->nullable() + ->after('from_school_year_id'); } + if (! Schema::hasColumn('parent_balance_transfers', 'source_summary')) { - $table->json('source_summary')->nullable()->after('source_summary_json'); + $table->json('source_summary') + ->nullable() + ->after('source_summary_json'); } + if (! Schema::hasColumn('parent_balance_transfers', 'old_balance_invoice_id')) { - $table->unsignedBigInteger('old_balance_invoice_id')->nullable()->after('new_invoice_id'); + $table->unsignedBigInteger('old_balance_invoice_id') + ->nullable() + ->after('new_invoice_id'); } + if (! Schema::hasColumn('parent_balance_transfers', 'reversed_at')) { $table->timestamp('reversed_at')->nullable(); } + if (! Schema::hasColumn('parent_balance_transfers', 'reversed_by')) { $table->unsignedBigInteger('reversed_by')->nullable(); } + if (! Schema::hasColumn('parent_balance_transfers', 'reversal_reason')) { $table->text('reversal_reason')->nullable(); } @@ -131,23 +188,36 @@ return new class extends Migration if (Schema::hasTable('invoices')) { Schema::table('invoices', function (Blueprint $table): void { if (! Schema::hasColumn('invoices', 'school_year_id')) { - $table->unsignedBigInteger('school_year_id')->nullable()->after('school_year'); + $table->unsignedBigInteger('school_year_id') + ->nullable(); } + if (! Schema::hasColumn('invoices', 'parent_id')) { $table->unsignedBigInteger('parent_id')->nullable(); } + if (! Schema::hasColumn('invoices', 'student_id')) { - $table->unsignedBigInteger('student_id')->nullable()->after('parent_id'); + $table->unsignedBigInteger('student_id') + ->nullable() + ->after('parent_id'); } + if (! Schema::hasColumn('invoices', 'invoice_type')) { - $table->string('invoice_type', 50)->nullable()->after('invoice_number'); + $table->string('invoice_type', 50) + ->nullable() + ->after('invoice_number'); } + if (! Schema::hasColumn('invoices', 'balance_transfer_id')) { - $table->unsignedBigInteger('balance_transfer_id')->nullable()->after('invoice_type'); + $table->unsignedBigInteger('balance_transfer_id') + ->nullable() + ->after('invoice_type'); } + if (! Schema::hasColumn('invoices', 'locked_at')) { $table->timestamp('locked_at')->nullable(); } + if (! Schema::hasColumn('invoices', 'locked_by')) { $table->unsignedBigInteger('locked_by')->nullable(); } @@ -159,4 +229,4 @@ return new class extends Migration { Schema::dropIfExists('student_enrollments'); } -}; +}; \ No newline at end of file diff --git a/database/migrations/2026_07_10_070529_add_school_year_fields_to_login_activity_table.php b/database/migrations/2026_07_10_070529_add_school_year_fields_to_login_activity_table.php new file mode 100644 index 00000000..ad036208 --- /dev/null +++ b/database/migrations/2026_07_10_070529_add_school_year_fields_to_login_activity_table.php @@ -0,0 +1,32 @@ +string('school_year', 50)->nullable(); + }); + } + } + + public function down(): void + { + if ( + Schema::hasTable('login_activity') + && Schema::hasColumn('login_activity', 'school_year') + ) { + Schema::table('login_activity', function (Blueprint $table): void { + $table->dropColumn('school_year'); + }); + } + } +}; \ No newline at end of file