diff --git a/app/Models/PayrollEntry.php b/app/Models/PayrollEntry.php index 15bb36f57..cae630883 100644 --- a/app/Models/PayrollEntry.php +++ b/app/Models/PayrollEntry.php @@ -66,6 +66,14 @@ class PayrollEntry extends BaseModel return $this->belongsTo(PayrollRun::class); } + /** + * Get the payslip for this payroll entry. + */ + public function payslip() + { + return $this->hasOne(Payslip::class, 'payroll_entry_id'); + } + /** * Get the employee. */ diff --git a/docs/PLAN-sidebar-payslip-fix.md b/docs/PLAN-sidebar-payslip-fix.md new file mode 100644 index 000000000..81bd4dbbc --- /dev/null +++ b/docs/PLAN-sidebar-payslip-fix.md @@ -0,0 +1,68 @@ +# Implementation Plan - Sidebar Brand Title & Payslip Generation Fix + +**Task Name:** Change Sidebar Brand Name & Fix Payslip Generation Error +**Target Plan File:** `docs/PLAN-sidebar-payslip-fix.md` +**Status:** Ready for Review + +--- + +## 1. Problem Analysis & Root Cause + +### Item A: Sidebar Brand Name ("SEB CONNEXION INC." → "SCX Software") +- **Mechanism:** In [app-sidebar.tsx](file:///Users/dvapp/Documents/HRM/resources/js/components/app-sidebar.tsx#L1100), when no custom brand logo is uploaded, the sidebar header renders `{titleText || 'WorkDo'}`. +- **Source of Data:** The `titleText` property is stored in the `settings` database table (`key = 'titleText'`) and exposed to frontend Inertia props via `settings()` / `useBrand()`. +- **Current DB Value:** Currently stores `"SEB CONNEXION INC."`. +- **Resolution Options:** + 1. **From Admin UI:** Navigate to **Settings** (`/settings`) → **Brand Settings** tab → Change **"Title Text"** to `SCX Software` → Click **Save Changes**. + 2. **Automated/Backend:** Update the `settings` table record directly for `titleText` to `SCX Software`. + +--- + +### Item B: Payslip Generation Error +- **Error:** `Failed to generate payslips: Call to undefined method App\Models\PayrollEntry::payslip()` +- **Mechanism:** In [PayslipController.php](file:///Users/dvapp/Documents/HRM/app/Http/Controllers/PayslipController.php#L314-L316), inside `bulkGenerate()`: + ```php + $pendingEntries = PayrollEntry::where('payroll_run_id', $payrollRun->id) + ->whereDoesntHave('payslip') + ->exists(); + ``` + Calling `whereDoesntHave('payslip')` requires an Eloquent relation method named `payslip()` on [PayrollEntry](file:///Users/dvapp/Documents/HRM/app/Models/PayrollEntry.php). +- **Missing Code:** [PayrollEntry.php](file:///Users/dvapp/Documents/HRM/app/Models/PayrollEntry.php) defines `payrollRun()`, `employee()`, `adjustments()`, and `creator()`, but **does not define** the inverse `payslip()` relation: + ```php + /** + * Get the payslip associated with this payroll entry. + */ + public function payslip() + { + return $this->hasOne(Payslip::class, 'payroll_entry_id'); + } + ``` + +--- + +## 2. Proposed Task Breakdown + +### Phase 1: Fix Eloquent Relation on `PayrollEntry` +- **File:** [app/Models/PayrollEntry.php](file:///Users/dvapp/Documents/HRM/app/Models/PayrollEntry.php) +- **Changes:** + - Add `public function payslip()` relationship method returning `$this->hasOne(Payslip::class, 'payroll_entry_id')`. + - Add typehints and docblock. + +### Phase 2: Update Sidebar Title Setting +- **Database / Command:** + - Update `Setting::where('key', 'titleText')->update(['value' => 'SCX Software'])` so current and future sessions immediately see **SCX Software**. + - Provide clear instructions for updating it anytime via **Settings > Brand Settings > Title Text**. + +### Phase 3: Verification & Automated Tests +- **Verification Commands:** + - Run unit/feature tests: `php artisan test --filter=PayslipTest` (or equivalent). + - Test Eloquent query resolution: verify `PayrollEntry::whereDoesntHave('payslip')->toSql()` runs without exception. + - Verify `bulkGenerate` payslip generation workflow succeeds and marks run as completed when all entries have payslips. + +--- + +## 3. Risk Assessment & Mitigations +- **Risk:** Does `Payslip` table have entries without `payroll_entry_id`? + - **Mitigation:** The `payslips` migration and model already define `payroll_entry_id` as foreign key. The `hasOne` relationship maps 1:1 with existing schema. +- **Risk:** Cache persistence of `settings()` helper. + - **Mitigation:** Clear configuration and app cache (`php artisan cache:clear`) after updating the `titleText` setting. diff --git a/tests/Feature/PayslipGenerationRelationTest.php b/tests/Feature/PayslipGenerationRelationTest.php new file mode 100644 index 000000000..081785320 --- /dev/null +++ b/tests/Feature/PayslipGenerationRelationTest.php @@ -0,0 +1,69 @@ +create(['type' => 'company']); + $employee = User::factory()->create(['type' => 'employee', 'created_by' => $company->id]); + + $payrollRun = PayrollRun::create([ + 'title' => 'Test Payroll Run', + 'payroll_frequency' => 'monthly', + 'pay_period_start' => '2026-09-01', + 'pay_period_end' => '2026-09-15', + 'pay_date' => '2026-09-15', + 'status' => 'processing', + 'created_by' => $company->id, + ]); + + $payrollEntry = PayrollEntry::create([ + 'payroll_run_id' => $payrollRun->id, + 'employee_id' => $employee->id, + 'basic_salary' => 15000, + 'net_pay' => 15000, + 'gross_pay' => 15000, + 'created_by' => $company->id, + ]); + + // Verify relationship method exists and query works + $pending = PayrollEntry::where('payroll_run_id', $payrollRun->id) + ->whereDoesntHave('payslip') + ->exists(); + + $this->assertTrue($pending); + + // Create payslip for entry + $payslip = Payslip::create([ + 'payroll_entry_id' => $payrollEntry->id, + 'employee_id' => $employee->id, + 'payslip_number' => 'PS-TEST-001', + 'pay_period_start' => '2026-09-01', + 'pay_period_end' => '2026-09-15', + 'pay_date' => '2026-09-15', + 'status' => 'generated', + 'created_by' => $company->id, + ]); + + $this->assertInstanceOf(Payslip::class, $payrollEntry->fresh()->payslip); + $this->assertEquals($payslip->id, $payrollEntry->fresh()->payslip->id); + + // Now whereDoesntHave should return false + $pendingAfter = PayrollEntry::where('payroll_run_id', $payrollRun->id) + ->whereDoesntHave('payslip') + ->exists(); + + $this->assertFalse($pendingAfter); + } +}