# APWP-AO Dashboard Section — Implementation Plan

## Build Status

| Component | Status | Notes |
|---|---|---|
| `PluginAccessService::userEverPurchasedSku()` | ✅ Done | |
| `DownloadController` | ✅ Done | `$aoLicenseCount` now returns 0 when lapsed (minor improvement over plan) |
| `downloads.blade.php` | ✅ Done | Section + JS replaced as two edits rather than four separate steps — same result |
| `dashboard.blade.php` | ✅ Done | See deviations: `@php` outside `@if` gate; `$aoUser` prefix; null guards; slot bg colour adjusted |
| Phase 5 static verification | ✅ Done | All 20 static checks pass. 10 interactive items require manual browser sign-off (see phases doc). |
| Phase 5 bug fix | ✅ Done | Gift-only users were not seeing the section. `$aoHasActiveAccess` was short-circuited by `$aoEverPurchased`. Fixed by decoupling and adding `$aoSectionVisible = $aoEverPurchased \|\| $aoHasActiveAccess` as the section gate. |

---

## Overview

Move the license-management UI for the `apwp-ao` (Admin Menu Organizer) plugin out of the Downloads page and into a dedicated section on the main dashboard. The section is gated to users who have ever purchased the plugin. When a subscription has lapsed, the section remains visible but is grayed out and non-interactive, with a "Reactivate Subscription" overlay button that adds the product back to the cart.

---

## What Moves

From `resources/views/account/downloads.blade.php`, the following unique blocks are **relocated** to the dashboard:

| Block | Lines (current) | Destination |
|---|---|---|
| Plugin key input (show/hide/copy) | 79–103 | Dashboard APWP-AO section |
| Connected Sites / slot rows | 118–156 | Dashboard APWP-AO section |
| Plugin key JS | 198–229 | Dashboard (bottom of view) |
| Dashboard message modal + JS | 231–388 | Dashboard (bottom of view) |
| Release slot JS | 391–426 | Dashboard (bottom of view) |

The apwp-ao card on the Downloads page retains: product header, description, "N site slots available" badge, Download button, and a contextual purchase/reactivate button (see Button Logic below).

---

## Button Logic

The label and meaning of the cart/action button changes based on the user's purchase and subscription state. The underlying action is always the same (`POST /cart/add` with the apwp-ao `product_id`), but the label and placement differ.

| Location | State | Button |
|---|---|---|
| Downloads page | Never purchased | "Purchase License" |
| Downloads page | Active subscription | "Purchase Additional Slots" |
| Downloads page | Lapsed subscription | "Reactivate Subscription" |
| Dashboard APWP-AO section | Active subscription | "Purchase Additional Slots" (below the slots list, always accessible) |
| Dashboard APWP-AO section | Lapsed subscription | "Reactivate Subscription" (centered overlay, content grayed behind it) |

The Downloads page needs `$aoEverPurchased`, `$aoHasActiveAccess`, and `$aoReactivateQty` passed from `DownloadController` to drive this logic. The dashboard section computes all of these inline.

### Reactivate quantity calculation

When reactivating, the cart quantity must cover all currently occupied slots at the rate of 1 license = 5 slots:

```
$aoReactivateQty = max(1, (int) ceil(occupied_slots / 5))
```

Examples: 0 slots → qty 1 (minimum) · 3 slots → qty 1 · 6 slots → qty 2 · 11 slots → qty 3

The `quantity` field is submitted as a hidden input alongside `product_id` on every "Reactivate Subscription" form. `CartController@add` already validates and accepts a `quantity` field (min:1).

---

## New Service Method

**File:** `app/Services/PluginAccessService.php`

Add `userEverPurchasedSku(User $user, string $sku): bool` after the existing `userHasAccessToSku()` method.

- Checks whether the user has **any completed order** containing the SKU — no payment-validity check.
- This answers "did they ever buy it?" independently of whether the subscription is currently active.
- Pattern: same order query used in `purchasedLicenseQty()` (line 124+), but use `->exists()` and skip the payment block entirely.
- Wrap in try/catch with `Log::warning` on failure (consistent with rest of service).

---

## Dashboard Data

**File:** `resources/views/dashboard.blade.php`

The dashboard route is a simple closure (no controller). Follow the existing inline `@php` pattern used for affiliates and gift subscriptions.

Add an `@php` block at the top of the new section:

```php
@php
    $user = auth()->user();
    $aoEverPurchased   = \App\Services\PluginAccessService::userEverPurchasedSku($user, 'apwp-ao');
    $aoHasActiveAccess = $aoEverPurchased && \App\Services\PluginAccessService::userHasAccessToSku($user, 'apwp-ao');
    $aoProduct         = \Vanilo\Product\Models\Product::where('sku', 'apwp-ao')->first();
    $aoPluginKey       = $user->ao_plugin_key ?? null;
    $aoLicenseCount    = $aoHasActiveAccess ? \App\Services\PluginAccessService::aoAllowedSlots($user) : 0;
    $aoActiveSlots     = $user->pluginAoDomains()
        ->where('is_active', true)
        ->orderBy('authorized_at')
        ->get(['id', 'domain_label', 'domain_url', 'authorized_at', 'last_connected_at']);
    $aoReactivateQty   = max(1, (int) ceil($aoActiveSlots->count() / 5));
@endphp
```

---

## Dashboard Section UI Structure

Append after the Gift Subscriptions card, inside the page wrapper:

```
<div class="mt-6 bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
  <div class="p-6 text-gray-900 dark:text-gray-100">
    <h3 class="text-lg font-medium mb-4">Admin Menu Organizer</h3>

    @if($aoEverPurchased)
      <div class="relative">

        {{-- Content layer — grayed + non-interactive when lapsed --}}
        <div class="{{ $aoHasActiveAccess ? '' : 'opacity-40 pointer-events-none select-none' }}">

          {{-- Plugin key block (moved from downloads, lines 79–103) --}}
          @if($aoPluginKey)
            <div class="mb-4 border-t border-gray-200 dark:border-gray-700 pt-4">
              <h5 ...>Your apwp-ao Plugin Key</h5>
              <p ...>Enter this key in each WordPress site ...</p>
              <div class="flex items-center gap-2">
                <input type="password" id="aamo-key-field" readonly value="{{ $aoPluginKey }}" ...>
                <button id="aamo-key-toggle" ...>Show</button>
                <button id="aamo-key-copy" ...>Copy</button>
              </div>
            </div>
          @endif

          {{-- Connected Sites / slot rows (moved from downloads, lines 118–156) --}}
          <div class="border-t border-gray-200 dark:border-gray-700 pt-4">
            <h5 ...>
              Connected Sites
              @if($aoLicenseCount > 0 && $aoActiveSlots->isNotEmpty())
                <span ...>({{ $aoActiveSlots->count() }} of {{ $aoLicenseCount }} slots used)</span>
              @endif
            </h5>
            @if($aoActiveSlots->isEmpty())
              <p ...>No sites connected yet.</p>
            @else
              <ul ...>
                @foreach($aoActiveSlots as $aoSlot)
                  <li ...>
                    {{ $aoSlot->domain_label ?? 'Site #' . ($loop->index + 1) }}
                    Last connected: ...
                    [Set Message] [Release] buttons
                  </li>
                @endforeach
              </ul>
            @endif
          </div>

          {{-- Purchase Additional Slots button — active state only, below slots list --}}
          <div class="mt-4 flex justify-end">
            @if($aoProduct)
              <form method="POST" action="{{ route('cart.add') }}">
                @csrf
                <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
                <button type="submit"
                  class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
                  Purchase Additional Slots
                </button>
              </form>
            @else
              <a href="{{ route('products.index') }}"
                class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
                Purchase Additional Slots
              </a>
            @endif
          </div>

        </div>{{-- end content layer --}}

        {{-- Reactivate overlay — only when lapsed --}}
        @if(!$aoHasActiveAccess)
          <div class="absolute inset-0 flex items-center justify-center">
            @if($aoProduct)
              <form method="POST" action="{{ route('cart.add') }}">
                @csrf
                <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
                <input type="hidden" name="quantity" value="{{ $aoReactivateQty }}">
                <button type="submit"
                  class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-lg hover:bg-blue-700">
                  Reactivate Subscription
                </button>
              </form>
            @else
              <a href="{{ route('products.index') }}"
                class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-lg hover:bg-blue-700">
                Reactivate Subscription
              </a>
            @endif
          </div>
        @endif

      </div>{{-- end relative wrapper --}}
    @endif

  </div>
</div>
```

### JS blocks at bottom of `dashboard.blade.php`

Move and adapt the three JS blocks from `downloads.blade.php`, updating the `@if` guards:

| Script | New guard condition |
|---|---|
| Plugin key show/hide/copy | `@if($aoHasActiveAccess && $aoPluginKey)` |
| Dashboard message modal + TinyMCE | `@if($aoHasActiveAccess && $aoActiveSlots->where('domain_url', '!=', null)->isNotEmpty())` |
| Release slot | `@if($aoHasActiveAccess && $aoActiveSlots->isNotEmpty())` |

The Dashboard message modal `<div>` markup (currently lines 232–262 of downloads.blade.php) also moves to the dashboard view, outside any `@if`, same as it is today on downloads.

---

## Downloads Page Cleanup

**File:** `resources/views/account/downloads.blade.php`

Remove from the `apwp-ao` `<section>`:
- Lines 79–103: the entire `@if($aoLicenseCount > 0)` plugin key block
- Lines 118–156: the "Connected Sites" block and surrounding `<div>`
- Lines 197–229: key show/hide/copy `<script>` and its `@if` guard
- Lines 231–388: dashboard message modal markup and JS
- Lines 391–426: release slot `<script>` and its `@if` guard

The slot count badge (line 71–72) remains on the downloads page. The purchase button form (lines 107–115) is replaced with a conditional block:

```blade
@if($aoHasActiveAccess)
  {{-- Active: offer additional slots --}}
  <form method="POST" action="{{ route('cart.add') }}" class="js-add-to-cart">
    @csrf
    <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
    <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
      Purchase Additional Slots
    </button>
  </form>
@elseif($aoEverPurchased)
  {{-- Lapsed: prompt to reactivate with qty to cover occupied slots --}}
  <form method="POST" action="{{ route('cart.add') }}" class="js-add-to-cart">
    @csrf
    <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
    <input type="hidden" name="quantity" value="{{ $aoReactivateQty }}">
    <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
      Reactivate Subscription
    </button>
  </form>
@else
  {{-- Never purchased: initial purchase CTA --}}
  @if($aoProduct)
    <form method="POST" action="{{ route('cart.add') }}" class="js-add-to-cart">
      @csrf
      <input type="hidden" name="product_id" value="{{ $aoProduct->id }}">
      <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
        Purchase License
      </button>
    </form>
  @else
    <a href="{{ route('products.index') }}" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
      Purchase License
    </a>
  @endif
@endif
```

This requires `$aoEverPurchased` and `$aoHasActiveAccess` to be passed from `DownloadController`.

---

## DownloadController Cleanup

**File:** `app/Http/Controllers/DownloadController.php`

Remove `$aoPluginKey` and `$aoActiveSlots` from the view data and their corresponding queries (lines 47–51). The `compact()` call on line 53 should no longer include those two variables.

Add the following to the view data:

```php
$aoEverPurchased   = PluginAccessService::userEverPurchasedSku($user, 'apwp-ao');
$aoHasActiveAccess = $aoEverPurchased && PluginAccessService::userHasAccessToSku($user, 'apwp-ao');
$aoOccupiedSlots   = PluginAccessService::aoUsedSlots($user); // existing method, returns int count
$aoReactivateQty   = max(1, (int) ceil($aoOccupiedSlots / 5));
```

`PluginAccessService::aoUsedSlots()` already exists (line 204 of the service) — no new query needed. Include `$aoEverPurchased`, `$aoHasActiveAccess`, and `$aoReactivateQty` in the `compact()` call along with the retained `$aoProduct` and `$aoLicenseCount`.

---

## Verification Checklist

1. **Active subscriber — Dashboard** — APWP-AO section shows plugin key (show/hide/copy), interactive slot rows (Set Message, Release), and a "Purchase Additional Slots" button below the list.
2. **Active subscriber — Downloads** — apwp-ao card shows "Purchase Additional Slots" button (not "Purchase License").
3. **Lapsed subscriber — Dashboard** — APWP-AO section is visible but grayed out; "Reactivate Subscription" button is centered in an overlay; clicking it adds apwp-ao to the cart with qty = `ceil(occupied_slots / 5)` (min 1).
4. **Lapsed subscriber — Downloads** — apwp-ao card shows "Reactivate Subscription" button; clicking it adds the same calculated quantity to the cart.
5. **Never purchased — Dashboard** — No APWP-AO section appears.
6. **Never purchased — Downloads** — apwp-ao card shows "Purchase License" button (original behavior).
7. **Slot release** — Clicking Release from the dashboard slot row fires DELETE to `/account/plugin-domains/{id}` and reloads correctly.
8. **Set Message** — Clicking Set Message from the dashboard opens the TinyMCE modal, loads the current message, and saves successfully.
