Commit 43259f2c authored by Mahmoud Aglan's avatar Mahmoud Aglan

Rename training_groups.program_id to training_program_id (naming convention)

Per project rule 01-migration-first.md, FK columns must follow
{singular_table}_id convention. Updates migration, models,
services, and all Livewire queries/views that reference this column.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 0cc5b982
......@@ -18,7 +18,7 @@ class TrainingGroup extends Model
use BelongsToAcademy, HasUuid, SoftDeletes, Auditable;
protected $fillable = [
'academy_id', 'program_id', 'branch_id',
'academy_id', 'training_program_id', 'branch_id',
'name', 'name_ar', 'code',
'head_trainer_id', 'max_capacity', 'current_count', 'waitlist_count',
'season', 'start_date', 'end_date',
......@@ -38,7 +38,7 @@ class TrainingGroup extends Model
public function program(): BelongsTo
{
return $this->belongsTo(TrainingProgram::class, 'program_id');
return $this->belongsTo(TrainingProgram::class, 'training_program_id');
}
public function branch(): BelongsTo
......
......@@ -75,7 +75,7 @@ public function creator(): BelongsTo
public function groups(): HasMany
{
return $this->hasMany(TrainingGroup::class, 'program_id');
return $this->hasMany(TrainingGroup::class, 'training_program_id');
}
public function enrollments(): HasMany
......
......@@ -41,7 +41,7 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act
// Guard: one group per program rule
$existingInProgram = Enrollment::where('participant_id', $participant->id)
->where('training_program_id', $group->program_id)
->where('training_program_id', $group->training_program_id)
->whereIn('status', ['pending', 'active'])
->exists();
if ($existingInProgram) {
......@@ -69,7 +69,7 @@ public function enroll(Participant $participant, TrainingGroup $group, User $act
$enrollment = Enrollment::create([
'participant_id' => $participant->id,
'training_group_id' => $group->id,
'training_program_id' => $group->program_id,
'training_program_id' => $group->training_program_id,
'enrollment_date' => now()->toDateString(),
'start_date' => $options['start_date'] ?? $group->start_date ?? now()->toDateString(),
'end_date' => $options['end_date'] ?? $group->end_date,
......@@ -124,7 +124,7 @@ public function enrollInProgram(Participant $participant, TrainingProgram $progr
private function findOrCreateGroup(TrainingProgram $program, User $actor, bool $allowOverload): TrainingGroup
{
// Find groups accepting enrollments (forming or active) with available capacity
$availableGroup = TrainingGroup::where('program_id', $program->id)
$availableGroup = TrainingGroup::where('training_program_id', $program->id)
->whereIn('status', ['forming', 'active'])
->lockForUpdate()
->get()
......@@ -139,7 +139,7 @@ private function findOrCreateGroup(TrainingProgram $program, User $actor, bool $
// No group with capacity found — handle overload or auto-create
if ($allowOverload) {
// Overload: pick the least-full group (even though all are full)
$leastFullGroup = TrainingGroup::where('program_id', $program->id)
$leastFullGroup = TrainingGroup::where('training_program_id', $program->id)
->whereIn('status', ['forming', 'active', 'full'])
->lockForUpdate()
->orderByRaw('(max_capacity - current_count) DESC')
......@@ -161,7 +161,7 @@ private function findOrCreateGroup(TrainingProgram $program, User $actor, bool $
*/
private function autoCreateGroup(TrainingProgram $program, User $actor): TrainingGroup
{
$existingCount = TrainingGroup::where('program_id', $program->id)->count();
$existingCount = TrainingGroup::where('training_program_id', $program->id)->count();
$sequenceNumber = $existingCount + 1;
$programName = $program->name_ar ?: $program->name;
......@@ -170,7 +170,7 @@ private function autoCreateGroup(TrainingProgram $program, User $actor): Trainin
return $this->groupService->create([
'academy_id' => $program->academy_id,
'program_id' => $program->id,
'training_program_id' => $program->id,
'branch_id' => $program->branch_id,
'name' => $groupName,
'name_ar' => $groupNameAr,
......@@ -260,7 +260,7 @@ public function transfer(Enrollment $enrollment, TrainingGroup $toGroup, User $a
$newEnrollment = Enrollment::create([
'participant_id' => $enrollment->participant_id,
'training_group_id' => $toGroup->id,
'training_program_id' => $toGroup->program_id,
'training_program_id' => $toGroup->training_program_id,
'enrollment_date' => now()->toDateString(),
'start_date' => now()->toDateString(),
'end_date' => $toGroup->end_date,
......
......@@ -22,7 +22,7 @@ class TrainingGroupService
public function create(array $data, User $actor): TrainingGroup
{
return DB::transaction(function () use ($data, $actor) {
$program = TrainingProgram::findOrFail($data['program_id']);
$program = TrainingProgram::findOrFail($data['training_program_id']);
return TrainingGroup::create(array_merge($data, [
'max_capacity' => $data['max_capacity'] ?? $program->max_participants,
......
......@@ -36,7 +36,7 @@ public function mount(?TrainingGroup $group = null): void
if ($group && $group->exists) {
$this->group = $group;
$this->editing = true;
$this->program_id = $group->program_id;
$this->program_id = $group->training_program_id;
$this->branch_id = $group->branch_id;
$this->name = $group->name ?? '';
$this->name_ar = $group->name_ar;
......@@ -104,7 +104,7 @@ public function save(TrainingGroupService $service): void
try {
$data = [
'program_id' => $this->program_id,
'training_program_id' => $this->program_id,
'branch_id' => $this->branch_id,
'name' => $this->name,
'name_ar' => $this->name_ar,
......
......@@ -84,7 +84,7 @@ public function render()
->orWhere('code', 'ilike', "%{$this->search}%");
}))
->when($this->status, fn ($q) => $q->where('status', $this->status))
->when($this->programFilter, fn ($q) => $q->where('program_id', $this->programFilter))
->when($this->programFilter, fn ($q) => $q->where('training_program_id', $this->programFilter))
->orderBy($this->sortBy, $this->sortDir);
return view('livewire.groups.group-list', [
......
......@@ -237,7 +237,7 @@ public function render()
$groups = TrainingGroup::query()
->orderBy('name_ar')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->when($this->programFilter, fn ($q) => $q->where('program_id', $this->programFilter))
->when($this->programFilter, fn ($q) => $q->where('training_program_id', $this->programFilter))
->get();
$programs = TrainingProgram::query()
......@@ -261,7 +261,7 @@ private function getFilteredSessionIds(mixed $branchId): Collection
return TrainingSession::query()
->when($this->groupFilter, fn ($q) => $q->where('training_group_id', $this->groupFilter))
->when(! $this->groupFilter && $this->programFilter, function ($q) {
$q->whereHas('group', fn ($gq) => $gq->where('program_id', $this->programFilter));
$q->whereHas('group', fn ($gq) => $gq->where('training_program_id', $this->programFilter));
})
->when($branchId, fn ($q) => $q->whereHas('group', fn ($gq) => $gq->where('branch_id', $branchId)))
->when($this->dateFrom, fn ($q) => $q->where('session_date', '>=', $this->dateFrom))
......
......@@ -100,7 +100,7 @@ public function render()
->where('session_date', '<=', $weekEnd)
->whereNotIn('status', ['cancelled', 'rescheduled'])
->when($branchId, fn ($q) => $q->whereHas('group', fn ($gq) => $gq->where('branch_id', $branchId)))
->when($this->programFilter, fn ($q) => $q->whereHas('group', fn ($gq) => $gq->where('program_id', $this->programFilter)))
->when($this->programFilter, fn ($q) => $q->whereHas('group', fn ($gq) => $gq->where('training_program_id', $this->programFilter)))
->when($this->trainerFilter, fn ($q) => $q->where('trainer_id', $this->trainerFilter))
->orderBy('start_time')
->get();
......@@ -148,7 +148,7 @@ private function getProgramColors($sessions): array
'bg-pink-100 border-pink-400 text-pink-800',
];
$programIds = $sessions->pluck('group.program_id')->unique()->values();
$programIds = $sessions->pluck('group.training_program_id')->unique()->values();
$map = [];
foreach ($programIds as $index => $programId) {
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('training_groups', function (Blueprint $table) {
$table->renameColumn('program_id', 'training_program_id');
});
}
public function down(): void
{
Schema::table('training_groups', function (Blueprint $table) {
$table->renameColumn('training_program_id', 'program_id');
});
}
};
# Column Mismatch Fix Plan
# Column Mismatch Fix Plan — COMPLETED
Audited: 2026-07-01
Executed: 2026-07-01
Commit: 0cc5b98
Source of truth: Live PostgreSQL DB at 18.192.166.221 (elcaptainsportsonly)
Scope: 58 models, 50 migrations, 70+ views/components checked against 1142 live columns
---
## CRITICAL (SQL errors — pages crash)
## ALL ITEMS RESOLVED
### C1. GlobalSearch.php — Invoice query uses non-existent columns
**File:** `app/Livewire/GlobalSearch.php`
**Lines:** 59-73
**Bugs:**
- Line 60: `->where('branch_id', $branchId)``invoices` table has NO `branch_id`
- Line 62: `->where('invoice_number', 'like', ...)` — column is `number`, not `invoice_number`
- Line 59: `Invoice::with('participant.person')` — relationship is `billable`, not `participant`
- Line 73: `$invoice->invoice_number` — should be `$invoice->number`
**Fix:**
```php
// Remove branch_id filter (invoices don't have it), fix column name, fix relationship
Invoice::with('billable')
->where('number', 'like', "%{$this->query}%")
->orWhere('contact_name', 'like', "%{$this->query}%")
```
---
### C2. RevenueWidget.php — `payment_method` doesn't exist on `payments`
**File:** `app/Livewire/Dashboard/RevenueWidget.php`
**Lines:** 45-47
**Bug:** `->select('payment_method', ...)->groupBy('payment_method')` — column is `method`
**Fix:** Replace `payment_method` with `method` in select/groupBy
---
### C3. FinancialReport.php — Raw SQL references wrong columns
**File:** `app/Livewire/Reports/FinancialReport.php`
**Bugs:**
- Line 67: `$payments->groupBy('payment_method')` — should be `->groupBy('method')`
- Line 72: `balance_due` — should be `due_amount`
- Line 88: `SUM(invoice_items.line_total)` — should be `SUM(invoice_items.total_amount)`
**Fix:** Replace all three column names
---
### C4. PaymentPlanCreate.php — queries and writes non-existent columns
**File:** `app/Livewire/Financial/PaymentPlanCreate.php`
**Bugs:**
- Line 127: `->where('balance_due', '>', 0)` — should be `->where('due_amount', '>', 0)`
- Line 98: writes `total_amount` to payment_plans — doesn't exist
- Line 99: writes `down_payment` to payment_plans — doesn't exist
- Line 100: writes `installment_count` — should be `total_installments`
- Line 104: writes `created_by` to payment_plans — doesn't exist
- Line 109: writes `academy_id` to installments — doesn't exist
- Line 110: writes `installment_number` — should be `sequence`
- Line 112: writes `paid_amount` to installments — doesn't exist
**Fix:** Rewrite the `createPlan()` method to match actual schema:
```php
// payment_plans columns: invoice_id, academy_id, total_installments, paid_installments,
// installment_amount, frequency, start_date, next_due_date, status, notes, metadata
// installments columns: payment_plan_id, payment_id, sequence, amount, due_date, status, paid_at
```
---
## HIGH (Wrong data displayed — features broken but page loads)
### H1. `$invoice->invoice_number` used in 8 locations — should be `$invoice->number`
| File | Line(s) |
|------|---------|
| `resources/views/print/invoice.blade.php` | 5, 70 |
| `resources/views/livewire/dashboard.blade.php` | 197 |
| `resources/views/livewire/financial/payment-plan-create.blade.php` | 22 |
| `resources/views/livewire/receptionist/collect-payment-wizard.blade.php` | 179, 366 |
| `resources/views/reports/daily-financial-print.blade.php` | 76 |
| `app/Livewire/GlobalSearch.php` | 73 |
**Fix:** Find-replace `->invoice_number` with `->number` in all files
---
### H2. `$invoice->balance_due` used in 6 locations — should be `$invoice->due_amount`
| File | Line(s) |
|------|---------|
| `resources/views/print/invoice.blade.php` | 158 |
| `resources/views/livewire/financial/payment-plan-create.blade.php` | 22, 31 |
| `app/Livewire/Financial/PaymentPlanCreate.php` | 60, 98 |
| `app/Livewire/Reports/FinancialReport.php` | 72 |
**Fix:** Find-replace `->balance_due` with `->due_amount` and `'balance_due'` with `'due_amount'`
---
### H3. `$participant->full_name_ar` in 7 locations — accessor doesn't exist
The `getFullNameAttribute()` already returns Arabic name. There is no `getFullNameArAttribute()`.
| File | Line(s) |
|------|---------|
| `resources/views/print/certificate.blade.php` | 39 |
| `resources/views/print/participant-card.blade.php` | 5, 42, 46 |
| `resources/views/print/group-schedule.blade.php` | 86 |
| `resources/views/print/invoice.blade.php` | 85 |
| `resources/views/livewire/participants/bulk-status-change.blade.php` | 70, 92 |
**Fix:** Replace `->full_name_ar` with `->full_name`
---
### H4. `$payment->payment_method` in print view — column is `method`
**File:** `resources/views/print/invoice.blade.php` line 179
**Fix:** Replace `$payment->payment_method` with `$payment->method`
---
### H5. `$invoice->subtotal` — should be `$invoice->subtotal_amount`
**File:** `resources/views/print/invoice.blade.php` line 127
**Fix:** Replace `->subtotal` with `->subtotal_amount`
---
### H6. `$invoice->description` — should be `$invoice->notes`
**File:** `resources/views/livewire/receptionist/collect-payment-wizard.blade.php` line 180
**Fix:** Replace `->description` with `->notes`
---
### H7. `$item->line_total` in print view — should be `$item->total_amount`
**File:** `resources/views/print/invoice.blade.php` line 116
**Fix:** Replace `$item->line_total` with `$item->total_amount`
---
### H8. Reports page — `$method['payment_method']` key doesn't exist
**File:** `resources/views/livewire/reports/reports-page.blade.php` line 92
**Fix:** Replace `$method['payment_method']` with `$method['method']`
---
### H9. `$payment->receipt_number` — doesn't exist on payments table
**File:** `app/Livewire/Receptionist/CollectPaymentWizard.php` line 165
**Fix:** Remove or use `$payment->reference` (actual column) or derive from POS transaction
---
## MEDIUM (Type errors / wrong table access)
### M1. Dashboard calls `->format('H:i')` on a plain string
**File:** `resources/views/livewire/dashboard.blade.php` line 169
**Bug:** `$session->start_time?->format('H:i')` — start_time is a string, not Carbon
**Fix:** Use `\Carbon\Carbon::parse($session->start_time)->format('H:i')` or add cast to model
---
### M2. Participant card accesses person fields directly on participant
**File:** `resources/views/print/participant-card.blade.php` lines 54, 57, 60, 63
**Bug:** `$participant->date_of_birth`, `$participant->gender` — these are on `people` table
**Fix:** Use `$participant->person->date_of_birth` and `$participant->person->gender`
| # | Issue | Status |
|---|-------|--------|
| C1 | GlobalSearch.php — non-existent branch_id, invoice_number, participant relationship | FIXED |
| C2 | RevenueWidget.php — payment_method → method | FIXED |
| C3 | FinancialReport.php — line_total, balance_due, payment_method | FIXED |
| C4 | PaymentPlanCreate.php — 8 wrong columns in mass assignment + queries | FIXED |
| H1 | invoice_number → number (8 locations) | FIXED |
| H2 | balance_due → due_amount (6 locations) | FIXED |
| H3 | full_name_ar → full_name (7 locations) | FIXED |
| H4 | payment_method → method in print view | FIXED |
| H5 | subtotal → subtotal_amount | FIXED |
| H6 | description → notes | FIXED |
| H7 | line_total → total_amount | FIXED |
| H8 | reports-page payment_method key → method | FIXED |
| H9 | receipt_number → reference | FIXED |
| M1 | Dashboard format() on string time | FIXED |
| M2 | Participant card person fields via relationship | FIXED |
| L1 | Dashboard participant → contact_name | FIXED |
| L2 | createdBy → creator | FIXED |
| MIG1 | cash_session_id added to payments (migration + live DB) | FIXED |
| MOD1 | float → decimal:2 casts | FIXED |
| MOD2 | Payment::cashSession() now valid (column exists) | FIXED |
| DOC1 | Enum registry updated to match DB | FIXED |
---
## LOW (Silent relationship failures)
### L1. Dashboard uses non-existent `$invoice->participant` relationship
**File:** `resources/views/livewire/dashboard.blade.php` line 197
**Bug:** `$inv->participant?->person?->name_ar` — relationship is `billable`
**Fix:** Use `$inv->billable?->person?->name_ar` or `$inv->contact_name`
---
### L2. Daily report uses `$payment->createdBy` — relationship is `creator`
**File:** `resources/views/reports/daily-financial-print.blade.php` line 77
**Fix:** Replace `$payment->createdBy` with `$payment->creator`
---
## MIGRATION NEEDED
### MIG1. Add `cash_session_id` to `payments` table
**Why:** Both `Payment::cashSession()` and `CashSession::payments()` reference this FK but it doesn't exist.
```php
Schema::table('payments', function (Blueprint $table) {
$table->foreignId('cash_session_id')->nullable()->after('branch_id')->constrained('cash_sessions')->nullOnDelete();
$table->index('cash_session_id');
});
```
Also add `'cash_session_id'` to Payment model `$fillable`.
---
## MODEL FIXES
### MOD1. Evaluation + EvaluationScore — float cast should be decimal
**Files:**
- `app/Domain/Training/Models/Evaluation.php` — change `'overall_score' => 'float'` to `'decimal:2'`
- `app/Domain/Training/Models/EvaluationScore.php` — change `'score' => 'float'` to `'decimal:2'`
---
### MOD2. Remove `Payment::cashSession()` relationship until migration runs
If migration MIG1 won't run immediately, the relationship should be commented out to prevent runtime errors.
---
## NAMING CONVENTION ISSUE (Non-breaking, future consideration)
## REMAINING TECH DEBT (non-breaking, future consideration)
### NAM1. `training_groups.program_id` should be `training_program_id`
Per project rules in `01-migration-first.md`. This is a rename that would require:
Per project rules in `01-migration-first.md`. Would require:
- Migration to rename column
- Update TrainingGroup model fillable + relationship
- Update all queries/views referencing `program_id`
- Risk: breaking change across many files
**Recommendation:** Low priority. Document as tech debt. The inconsistency works; fixing it mid-development risks introducing bugs.
---
## DOCUMENTATION DRIFT (Update rule files)
### DOC1. Enum registry in `16-enums-and-checks.md` is outdated
| Enum | Documented | Actual (migration) |
|------|-----------|-------------------|
| InvoiceStatus | 7 values | 13 values (add: pending, awaiting_approval, overpaid, partially_refunded, written_off, disputed) |
| PaymentStatus | 5 values | 5 values but different (has `cancelled` instead of `partially_refunded`) |
| PaymentMethod | 6 values | 7 values (add: `online`) |
**Fix:** Update `.claude/rules/16-enums-and-checks.md` to match actual migration values.
---
## EXECUTION ORDER
1. **C1-C4** — Fix critical SQL errors (pages literally crash)
2. **H1-H9** — Fix wrong column names (features show null/empty data)
3. **M1-M2** — Fix type errors and wrong table access
4. **L1-L2** — Fix silent relationship failures
5. **MIG1** — Add missing migration + update model fillable
6. **MOD1-MOD2** — Fix model casts
7. **DOC1** — Update rule file documentation
8. **NAM1** — Consider later (tech debt)
---
## STATS
| Category | Count |
|----------|-------|
| Files to modify | ~18 |
| Critical (crashes) | 4 issues |
| High (broken features) | 9 issues |
| Medium (conditional errors) | 2 issues |
| Low (silent failures) | 2 issues |
| Migrations needed | 1 |
| Model fixes | 2 |
| Doc updates | 1 |
| **Total fixes** | **21** |
**Status:** Deferred. The inconsistency works; fixing mid-development risks introducing bugs.
......@@ -129,7 +129,7 @@ class="w-full rounded-lg border-gray-300 text-sm shadow-sm focus:border-blue-500
$endMin = (int) substr($session->end_time, 3, 2);
$durationMinutes = ($endHour * 60 + $endMin) - ($startHour * 60 + $startMin);
$heightRem = max(3.5, ($durationMinutes / 60) * 3.75);
$colorClass = $programColors[$session->group?->program_id] ?? 'bg-gray-100 border-gray-400 text-gray-800';
$colorClass = $programColors[$session->group?->training_program_id] ?? 'bg-gray-100 border-gray-400 text-gray-800';
@endphp
<div
class="mb-1 overflow-hidden rounded-md border-s-4 p-1.5 text-xs {{ $colorClass }}"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment