# Admin Customers List + Detail 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:** Build the admin customer DataTable (`/api/v1/admin/customers`) and customer detail page (`/api/v1/admin/customers/{customer}`), plus a `customer_id` filter on `/api/v1/admin/reservations`.

**Architecture:** Add a new `Admin\CustomerController` under the existing admin route group. Reuse `CustomerResource` by adding `reservations_count` only when counted. Extend the existing admin reservation filtering pipeline with a `customer_id` parameter.

**Tech Stack:** Laravel 11, PHP 8.3, Sanctum, Spatie permissions, Pest.

---

### Task 1: Create `ListAdminCustomersRequest`

**Files:**
- Create: `app/Http/Requests/Admin/ListCustomersRequest.php`

- [ ] **Step 1: Write the request class**

```php
<?php

namespace App\Http\Requests\Admin;

use Illuminate\Foundation\Http\FormRequest;

class ListCustomersRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'q' => ['nullable', 'string', 'max:255'],
            'is_verified' => ['nullable', 'boolean'],
            'has_wallet' => ['nullable', 'boolean'],
            'created_from' => ['nullable', 'date'],
            'created_to' => ['nullable', 'date', 'after_or_equal:created_from'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ];
    }
}
```

---

### Task 2: Create `Admin\CustomerController`

**Files:**
- Create: `app/Http/Controllers/Admin/CustomerController.php`

- [ ] **Step 1: Write the controller**

```php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ListCustomersRequest;
use App\Http\Resources\CustomerResource;
use App\Models\Customer;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Resources\Json\ResourceCollection;

class CustomerController extends Controller
{
    use AuthorizesRequests;

    public function index(ListCustomersRequest $request): ResourceCollection
    {
        $this->authorize('viewAny', Customer::class);

        $query = Customer::query()
            ->with('user')
            ->withCount('reservations')
            ->when($request->filled('q'), function ($q) use ($request) {
                $term = '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $request->input('q')).'%';
                $q->where(function ($w) use ($term) {
                    $w->where('phone', 'like', $term)
                        ->orWhereHas('user', function ($u) use ($term) {
                            $u->where('name', 'like', $term)
                                ->orWhere('email', 'like', $term)
                                ->orWhere('whatsapp_number', 'like', $term);
                        });
                });
            })
            ->when($request->has('is_verified'), function ($q) use ($request) {
                $q->whereHas('user', fn ($u) => $u->where('is_verified', $request->boolean('is_verified')));
            })
            ->when($request->has('has_wallet'), function ($q) use ($request) {
                if ($request->boolean('has_wallet')) {
                    $q->where('wallet', '>', 0);
                } else {
                    $q->where('wallet', '=', 0);
                }
            })
            ->when($request->filled('created_from'), function ($q) use ($request) {
                $q->whereDate('customers.created_at', '>=', $request->date('created_from'));
            })
            ->when($request->filled('created_to'), function ($q) use ($request) {
                $q->whereDate('customers.created_at', '<=', $request->date('created_to'));
            });

        return CustomerResource::collection(
            $query->latest('customers.created_at')->paginate($request->integer('per_page', 15))
        );
    }

    public function show(Customer $customer): CustomerResource
    {
        $this->authorize('view', $customer);

        $customer->load([
            'user',
            'reservations' => fn ($q) => $q->latest()->limit(15),
            'reservations.unit:id,building_id,name_or_number',
            'reservations.unit.building:id,name',
        ]);
        $customer->loadCount('reservations');

        return CustomerResource::make($customer);
    }
}
```

---

### Task 3: Update `CustomerResource`

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

- [ ] **Step 1: Add `reservations_count` and format dates**

```php
return [
    'id' => $this->id,
    'phone' => $this->phone,
    'whatsapp_number' => $this->user?->whatsapp_number,
    'wallet' => $this->wallet,
    'reservations_count' => $this->whenCounted('reservations'),
    'user' => UserResource::make($this->whenLoaded('user')),
    'reservations' => ReservationResource::collection($this->whenLoaded('reservations')),
    'created_at' => $this->created_at?->toIso8601String(),
];
```

- [ ] **Step 2: Add `use App\Models\Customer;` and `@mixin Customer` docblock (or add baseline ignores if the project prefers baseline)**

If the project uses `@mixin` like other resources:

```php
use App\Models\Customer;

/** @mixin Customer */
class CustomerResource extends JsonResource
```

If PHPStan baseline is preferred, add ignores for `$id`, `$phone`, `$wallet`, `$created_at`, `$reservations`, `$user` instead.

---

### Task 4: Add routes

**Files:**
- Modify: `routes/api.php`

- [ ] **Step 1: Import the controller at the top**

```php
use App\Http\Controllers\Admin\CustomerController as AdminCustomerController;
```

- [ ] **Step 2: Register routes inside the admin middleware group**

```php
Route::apiResource('admin/customers', AdminCustomerController::class)
    ->only(['index', 'show'])
    ->names('admin.customers');
```

---

### Task 5: Add `customer_id` filter to admin reservations

**Files:**
- Modify: `app/Http/Requests/ListReservationsRequest.php`
- Modify: `app/Services/Filter/FilterService.php`

- [ ] **Step 1: Add validation rule**

```php
'customer_id' => ['nullable', 'integer', 'exists:customers,id'],
```

- [ ] **Step 2: Apply filter in `applyToAdminReservationQuery`**

```php
if ($customerId = $filters['customer_id'] ?? null) {
    $query->whereHasMorph('customer', [Customer::class], fn (Builder $q) => $q->where('id', $customerId));
}
```

---

### Task 6: Write feature tests

**Files:**
- Create: `tests/Feature/Admin/CustomerListTest.php`
- Create: `tests/Feature/Admin/CustomerDetailTest.php`
- Modify: `tests/Feature/Admin/ReservationListTest.php`

- [ ] **Step 1: `CustomerListTest.php`**

Tests:
1. Admin can list customers and sees expected shape.
2. Non-admin cannot list.
3. Search filters by name/email/phone/whatsapp.
4. `is_verified` filter works.
5. `has_wallet` filter works.
6. `created_from`/`created_to` filter works.
7. `per_page` changes page size.

- [ ] **Step 2: `CustomerDetailTest.php`**

Tests:
1. Admin can view detail and sees nested user and reservations.
2. Non-admin cannot view.
3. 404 for missing customer.
4. `reservations_count` is correct.

- [ ] **Step 3: `ReservationListTest.php`**

Add a test that `?customer_id=` returns only that customer's reservations.

---

### Task 7: Update LRD docs

**Files:**
- Modify: `docs/api/roles/admin.md`

- [ ] **Step 1: Add the new endpoints and samples to the "Users & owners" or a new "Customers" subsection.**

---

### Task 8: Run verification

- [ ] **Step 1: Run tests**

```bash
php artisan test tests/Feature/Admin/CustomerListTest.php tests/Feature/Admin/CustomerDetailTest.php tests/Feature/Admin/ReservationListTest.php
```

Expected: all pass.

- [ ] **Step 2: Run full suite**

```bash
php artisan test --compact
```

Expected: all pass.

- [ ] **Step 3: Run PHPStan**

```bash
vendor/bin/phpstan analyse --no-progress --memory-limit=2G
```

Expected: no errors.

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

```bash
vendor/bin/pint --test
```

Expected: clean.
