# Employee Many Buildings Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Allow an employee to be assigned to multiple buildings via a pivot table while keeping the API backward-compatible with a single `building_id` field.

**Architecture:** Replace the nullable `employees.building_id` foreign key with a `building_employee` pivot table, update the `Employee` model to a `BelongsToMany` relationship, and normalize `building_id` / `building_ids` input in the employee form requests. Sync the pivot table on employee store/update and adjust the index filter to query the pivot.

**Tech Stack:** Laravel 13, Eloquent, Spatie permissions, Pest PHP, SQLite/MySQL.

---

### Task 1: Create pivot-table migration

**Files:**
- Create: `database/migrations/2026_06_29_123456_create_building_employee_table.php`

**Steps:**

- [ ] **Step 1: Write migration**

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('building_employee', function (Blueprint $table) {
            $table->id();
            $table->foreignId('employee_id')->constrained('employees')->cascadeOnDelete();
            $table->foreignId('building_id')->constrained('buildings')->cascadeOnDelete();
            $table->unique(['employee_id', 'building_id']);
            $table->timestamps();
        });

        $rows = DB::table('employees')
            ->whereNotNull('building_id')
            ->select('id as employee_id', 'building_id')
            ->get();

        foreach ($rows as $row) {
            DB::table('building_employee')->insert([
                'employee_id' => $row->employee_id,
                'building_id' => $row->building_id,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }

        Schema::table('employees', function (Blueprint $table) {
            $table->dropForeign(['building_id']);
            $table->dropColumn('building_id');
        });
    }

    public function down(): void
    {
        Schema::table('employees', function (Blueprint $table) {
            $table->foreignId('building_id')->nullable()->constrained('buildings')->after('owner_id');
        });

        $rows = DB::table('building_employee')
            ->select('employee_id', 'building_id')
            ->get();

        foreach ($rows as $row) {
            DB::table('employees')
                ->where('id', $row->employee_id)
                ->update(['building_id' => $row->building_id]);
        }

        Schema::dropIfExists('building_employee');
    }
};
```

- [ ] **Step 2: Run migration**

```bash
/c/Users/hamma/.config/herd/bin/php84/php.exe artisan migrate
```

Expected: migration succeeds with no errors.

---

### Task 2: Update Employee and Building models

**Files:**
- Modify: `app/Models/Employee.php`
- Modify: `app/Models/Building.php`

**Steps:**

- [ ] **Step 1: Replace `building()` with `buildings()` in Employee**

In `app/Models/Employee.php`:

```php
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

public function buildings(): BelongsToMany
{
    return $this->belongsToMany(Building::class)->withTimestamps();
}
```

Remove the old `building(): BelongsTo` method and the unused `BelongsTo` import if it is no longer needed.

- [ ] **Step 2: Add inverse relation on Building**

In `app/Models/Building.php` add:

```php
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

public function employees(): BelongsToMany
{
    return $this->belongsToMany(Employee::class)->withTimestamps();
}
```

---

### Task 3: Normalize building input in form requests

**Files:**
- Modify: `app/Http/Requests/Owner/StoreEmployeeRequest.php`
- Modify: `app/Http/Requests/Owner/UpdateEmployeeRequest.php`

**Steps:**

- [ ] **Step 1: Update StoreEmployeeRequest rules and validation**

Replace the `building_id` rule with:

```php
'building_id' => ['nullable', 'integer', 'exists:buildings,id'],
'building_ids' => ['nullable', 'array'],
'building_ids.*' => ['integer', 'exists:buildings,id'],
```

Add a helper method to the request:

```php
public function buildingIds(): array
{
    $ids = [];

    if ($this->filled('building_ids')) {
        $ids = array_map('intval', $this->input('building_ids'));
    }

    if ($this->filled('building_id')) {
        $ids[] = (int) $this->input('building_id');
    }

    return array_values(array_unique(array_filter($ids)));
}
```

Update the `withValidator` closure to validate every ID in `$this->buildingIds()` against the owner.

- [ ] **Step 2: Update UpdateEmployeeRequest**

Apply the same `building_id` / `building_ids` rules and the same `buildingIds()` helper.
Update the `withValidator` closure to validate every ID in the normalized list.

---

### Task 4: Update EmployeeController store/update/index

**Files:**
- Modify: `app/Http/Controllers/Api/Owner/EmployeeController.php`

**Steps:**

- [ ] **Step 1: Update store method**

After creating the employee, sync buildings:

```php
$employee->buildings()->sync($request->buildingIds());
```

Remove the direct `building_id` assignment from the `Employee::create()` payload.

- [ ] **Step 2: Update update method**

After updating scalar fields, sync buildings if any building input is present:

```php
if ($request->hasAny(['building_id', 'building_ids'])) {
    $employee->buildings()->sync($request->buildingIds());
}
```

Remove `building_id` from the `$employeeData` array intersection.

- [ ] **Step 3: Update index method**

Replace the `building_id` filter block with:

```php
->when($request->filled('building_id'), function ($q) use ($request) {
    $value = $request->input('building_id');

    if ($value === 'unassigned') {
        $q->whereDoesntHave('buildings');
    } else {
        $q->whereHas('buildings', fn ($b) => $b->where('buildings.id', (int) $value));
    }
})
```

---

### Task 5: Update EmployeeResource

**Files:**
- Modify: `app/Http/Resources/EmployeeResource.php`

**Steps:**

- [ ] **Step 1: Replace `building_id` with buildings list**

```php
return [
    'id' => $this->id,
    // ... keep other existing fields ...
    'building_ids' => $this->buildings->pluck('id')->values(),
    'buildings' => $this->buildings->map(fn ($b) => ['id' => $b->id, 'name' => $b->name]),
];
```

Remove the old `building_id` field and the eager-loaded `building` relation if it is no longer used.

---

### Task 6: Update existing tests

**Files:**
- Modify: `tests/Feature/Owner/EmployeeStoreTest.php`
- Modify: `tests/Feature/Owner/EmployeeUpdateTest.php`
- Modify: `tests/Feature/Owner/EmployeeListFiltersTest.php`

**Steps:**

- [ ] **Step 1: EmployeeStoreTest**

Update the success test to assert on `building_ids`:

```php
$response->assertCreated();
$employee = Employee::find($response->json('data.id'));
expect($employee->buildings->pluck('id')->all())->toEqualCanonicalizing([$this->building->id]);
```

Add a test that sends multiple buildings:

```php
it('allows an owner to create an employee assigned to multiple buildings', function () {
    $buildingB = Building::create([
        'name' => 'Second Building',
        'owner_id' => $this->owner->id,
        'region_id' => $this->building->region_id,
        'currency_id' => $this->building->currency_id,
        'check_in_time' => '14:00',
        'check_out_time' => '12:00',
        'status' => 'active',
        'payment_methods' => ['cash'],
        'slug' => 'second-building',
    ]);

    $response = $this->withToken($this->ownerToken)->postJson('/api/v1/employees', [
        'name' => 'Multi Building Employee',
        'email' => 'multi@example.com',
        'phone' => '+12025550300',
        'password' => 'Password123!',
        'building_ids' => [$this->building->id, $buildingB->id],
    ]);

    $response->assertCreated();
    expect($response->json('data.building_ids'))->toEqualCanonicalizing([$this->building->id, $buildingB->id]);
});
```

- [ ] **Step 2: EmployeeUpdateTest**

Update the existing cross-owner building test to use `building_ids` and assert the pivot is rejected.
Add a test that updates an employee to multiple buildings.

- [ ] **Step 3: EmployeeListFiltersTest**

Update the building filter test to create an employee with multiple buildings and assert filtering works.
Keep the `unassigned` and cross-owner tests.

---

### Task 7: Run verification

**Files:**
- All of the above

**Steps:**

- [ ] **Step 1: Run full Pest suite**

```bash
/c/Users/hamma/.config/herd/bin/php84/php.exe artisan test
```

Expected: all tests pass.

- [ ] **Step 2: Run Pint**

```bash
/c/Users/hamma/.config/herd/bin/php84/php.exe vendor/bin/pint --test
```

Expected: no style issues.

- [ ] **Step 3: Rollback and re-run migration**

```bash
/c/Users/hamma/.config/herd/bin/php84/php.exe artisan migrate:rollback --step=1
/c/Users/hamma/.config/herd/bin/php84/php.exe artisan migrate
```

Expected: rollback restores `building_id` and re-migration creates the pivot table without errors.

---

## Self-review

- Spec requirement: pivot table + drop `building_id` → Task 1.
- Spec requirement: `Employee::buildings()` → Task 2.
- Spec requirement: backward-compatible `building_id` / `building_ids` → Task 3.
- Spec requirement: sync on store/update → Task 4.
- Spec requirement: resource returns `building_ids` and `buildings` → Task 5.
- Spec requirement: index filter via pivot → Task 4.
- Spec requirement: tests → Task 6.
- No placeholders or TBDs.
