# Admin & Customer Filtering 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:** Add admin-scoped reservations/buildings indexes and validated filters for customer-scoped reservations/receipts, keeping each user’s data isolated.

**Architecture:** Two new admin routes reuse existing controllers (`ReservationController::adminIndex`, `BuildingController::adminIndex`). `FilterService` gets scope-specific filter methods. Controllers scope the query first, then hand it to `FilterService`. Existing customer/owner endpoints remain unchanged in behavior except for added filter support.

**Tech Stack:** Laravel 11, Pest 4, Spatie permissions, SQLite test DB.

---

## Task 1: Create `ListBuildingsRequest`

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

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

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

    public function rules(): array
    {
        return [
            'status' => ['nullable', 'string', Rule::in(['active', 'inactive', 'hidden'])],
            'owner_id' => ['nullable', 'integer', 'exists:owners,id'],
            'region_id' => ['nullable', 'integer', 'exists:regions,id'],
            'city_id' => ['nullable', 'integer', 'exists:cities,id'],
            'country_id' => ['nullable', 'integer', 'exists:countries,id'],
            'q' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ];
    }
}
```

---

## Task 2: Create `ListReceiptsRequest`

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

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

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

    public function rules(): array
    {
        return [
            'payment_status' => ['nullable', 'string', Rule::in(['paid', 'partially_paid', 'unpaid'])],
            'from' => ['nullable', 'date'],
            'to' => ['nullable', 'date', 'after_or_equal:from'],
            'has_balance_due' => ['nullable', 'boolean'],
            'q' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ];
    }
}
```

---

## Task 3: Extend `ListReservationsRequest` with `tab`

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

Add to the `rules()` array:

```php
'tab' => ['nullable', 'string', Rule::in(['all', 'upcoming', 'past'])],
```

---

## Task 4: Add filter methods to `FilterService`

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

Add after `applyToOwnerCalendarQuery`:

```php
public function applyToAdminReservationQuery(Builder $query, array $filters): Builder
{
    $this->applyReservationCommonFilters($query, $filters);

    if ($ownerId = $filters['owner_id'] ?? null) {
        $query->whereHas('unit.building', fn (Builder $q) => $q->where('owner_id', $ownerId));
    }

    return $query;
}

public function applyToAdminBuildingQuery(Builder $query, array $filters): Builder
{
    if ($status = $filters['status'] ?? null) {
        $query->where('status', $status);
    }

    if ($ownerId = $filters['owner_id'] ?? null) {
        $query->where('owner_id', $ownerId);
    }

    if ($regionId = $filters['region_id'] ?? null) {
        $query->where('region_id', $regionId);
    }

    if ($cityId = $filters['city_id'] ?? null) {
        $query->whereHas('region', fn (Builder $q) => $q->where('city_id', $cityId));
    }

    if ($countryId = $filters['country_id'] ?? null) {
        $query->whereHas('region.city', fn (Builder $q) => $q->where('country_id', $countryId));
    }

    if (! empty($filters['q'])) {
        $query->where('name', 'like', '%'.$filters['q'].'%');
    }

    return $query;
}

public function applyToCustomerReservationQuery(Builder $query, array $filters): Builder
{
    $this->applyReservationCommonFilters($query, $filters);

    $tab = $filters['tab'] ?? 'all';
    match ($tab) {
        'upcoming' => $query->whereDate('check_in_date', '>=', now()),
        'past' => $query->whereDate('check_in_date', '<', now()),
        default => null,
    };

    return $query;
}

public function applyToCustomerReceiptQuery(Builder $query, array $filters): Builder
{
    if ($status = $filters['payment_status'] ?? null) {
        match ($status) {
            'paid' => $query->where('remaining_amount', 0),
            'unpaid' => $query->where('paid_amount', 0),
            'partially_paid' => $query->where('paid_amount', '>', 0)->where('remaining_amount', '>', 0),
        };
    }

    $this->applyDateRangeFilters($query, $filters, 'created_at');

    if (array_key_exists('has_balance_due', $filters) && $filters['has_balance_due'] !== null) {
        if (filter_var($filters['has_balance_due'], FILTER_VALIDATE_BOOLEAN)) {
            $query->where('remaining_amount', '>', 0);
        } else {
            $query->where('remaining_amount', 0);
        }
    }

    if (! empty($filters['q'])) {
        $query->where('document_number', 'like', '%'.$filters['q'].'%');
    }

    return $query;
}

private function applyReservationCommonFilters(Builder $query, array $filters): void
{
    if (! empty($filters['status'])) {
        $query->whereIn('status', (array) $filters['status']);
    }

    if (! empty($filters['payment_status'])) {
        $query->whereIn('payment_status', (array) $filters['payment_status']);
    }

    if ($buildingId = $filters['building_id'] ?? null) {
        $query->whereHas('unit', fn (Builder $q) => $q->where('building_id', $buildingId));
    }

    if ($unitId = $filters['unit_id'] ?? null) {
        $query->where('unit_id', $unitId);
    }

    if ($from = $filters['check_in_from'] ?? null) {
        $query->whereDate('check_in_date', '>=', $from);
    }

    if ($to = $filters['check_in_to'] ?? null) {
        $query->whereDate('check_in_date', '<=', $to);
    }

    if ($from = $filters['check_out_from'] ?? null) {
        $query->whereDate('check_out_date', '>=', $from);
    }

    if ($to = $filters['check_out_to'] ?? null) {
        $query->whereDate('check_out_date', '<=', $to);
    }

    if ($from = $filters['created_from'] ?? null) {
        $query->whereDate('created_at', '>=', $from);
    }

    if ($to = $filters['created_to'] ?? null) {
        $query->whereDate('created_at', '<=', $to);
    }

    if (! empty($filters['source'])) {
        $query->where('source', $filters['source']);
    }

    if (! empty($filters['q'])) {
        $like = '%'.$filters['q'].'%';
        $query->where(fn (Builder $w) => $w
            ->where('reservation_number', 'like', $like)
            ->orWhereHasMorph('customer', [Customer::class], fn ($c) => $c->whereHas('user', fn ($u) => $u->where('name', 'like', $like)))
            ->orWhereHasMorph('customer', [PendingCustomer::class], fn ($c) => $c->where('name', 'like', $like))
            ->orWhereHas('unit', fn ($u) => $u->where('name_or_number', 'like', $like)));
    }
}
```

Import `App\Models\Customer` and `App\Models\PendingCustomer` at the top of `FilterService.php` if not already imported.

---

## Task 5: Add admin reservation route and controller method

**Files:**
- Modify: `routes/api.php`
- Modify: `app/Http/Controllers/ReservationController.php`

In `routes/api.php`, inside the `role:super_admin|admin` group add:

```php
Route::get('admin/reservations', [ReservationController::class, 'adminIndex']);
```

In `ReservationController.php`, add:

```php
use App\Facades\FilterService;
```

Add method:

```php
public function adminIndex(ListReservationsRequest $request)
{
    $this->authorize('viewAny', Reservation::class);

    $query = Reservation::query()
        ->with([
            'unit:id,building_id,name_or_number',
            'unit.building:id,name,owner_id',
            'customer' => fn ($q) => $q->morphWith([
                Customer::class => ['user'],
            ]),
        ]);

    $query = FilterService::applyToAdminReservationQuery($query, $request->validated());

    return ReservationResource::collection(
        $query->latest()->paginate($request->integer('per_page', 15))
    );
}
```

Update the customer branch of the existing `index()` method to call `FilterService::applyToCustomerReservationQuery($query, $request->validated())` before paginating.

---

## Task 6: Add admin building route and controller method

**Files:**
- Modify: `routes/api.php`
- Modify: `app/Http/Controllers/BuildingController.php`

In `routes/api.php`, inside the `role:super_admin|admin` group add:

```php
Route::get('admin/buildings', [BuildingController::class, 'adminIndex']);
```

In `BuildingController.php`, add:

```php
use App\Facades\FilterService;
```

Add method:

```php
public function adminIndex(ListBuildingsRequest $request)
{
    $this->authorize('viewAny', Building::class);

    $query = Building::with($this->buildingLoadSet());
    $query = FilterService::applyToAdminBuildingQuery($query, $request->validated());

    return BuildingResource::collection($query->paginate($request->integer('per_page', 5)));
}
```

---

## Task 7: Add receipt filters to `ReceiptController`

**Files:**
- Modify: `app/Http/Controllers/ReceiptController.php`

Change the method signature to `index(ListReceiptsRequest $request)` and update the customer branch:

```php
public function index(ListReceiptsRequest $request)
{
    $this->authorize('viewAny', Receipt::class);

    $user = auth()->user();
    $query = Receipt::latest();

    if ($user->isCustomer()) {
        $customer = $user->customer;
        $query->whereHas('reservation', function ($q) use ($customer) {
            $q->where('customer_type', Customer::class)
                ->where('customer_id', $customer->id);
        });

        $query = FilterService::applyToCustomerReceiptQuery($query, $request->validated());
    } elseif ($user->isApprovedOwner() || $user->isActiveEmployee()) {
        $ownerId = $user->isApprovedOwner() ? $user->owner?->id : $user->employee?->owner?->id;
        if ($ownerId) {
            $query->whereHas('reservation.unit.building', fn ($q) => $q->where('owner_id', $ownerId));
        } else {
            $query->whereRaw('1 = 0');
        }
    }

    return ReceiptResource::collection($query->paginate($request->integer('per_page', 15)));
}
```

Import `App\Facades\FilterService` and `App\Http\Requests\ListReceiptsRequest`.

---

## Task 8: Add feature tests for admin reservations

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

Test that an admin can list all reservations and filter by status/payment_status/building_id. Use existing factories/seeders and create two owners with one reservation each. Assert both reservations appear and filters reduce the result set.

---

## Task 9: Add feature tests for admin buildings

**Files:**
- Create: `tests/Feature/Admin/BuildingListTest.php`

Test that an admin can list buildings from multiple owners and filter by owner_id/status/q.

---

## Task 10: Add feature tests for customer reservation filters

**Files:**
- Create: `tests/Feature/Customer/ReservationListFiltersTest.php`

Create a customer with multiple reservations (different statuses, dates). Test:
- `tab=upcoming` returns only future check-ins.
- `status[]=checked_in` returns only matching reservations.
- `q=RSV-...` searches reservation number.
- Results never include another customer’s reservations.

---

## Task 11: Add feature tests for customer receipt filters

**Files:**
- Create: `tests/Feature/Customer/ReceiptListFiltersTest.php`

Create receipts for the authenticated customer with paid/partially/unpaid snapshots and different document numbers. Test payment_status, has_balance_due, q, and from/to filters are scoped to the customer.

---

## Task 12: Run full test suite and style checker

Run:

```bash
php84.bat artisan test
vendor/bin/pint.bat
```

Fix any failures before finishing.
