# Admin QOL Features - Design Document

**Date:** 2025-12-15
**Status:** Planning Complete, Ready for Implementation
**Branch:** `claude/admin-qol-features-01Wb8zZymEEmNJmaWwoXJcBo`

## Overview

This document captures the complete design for admin dashboard QOL improvements, reducing the need for developers to make direct database changes for common support tasks.

## Design Decisions

### 1. Feature Override Model
**Decision:** Binary feature grants with unique constraint per dealer+feature

Feature overrides grant access to specific premium features regardless of tier (e.g., give a Starter dealer access to Custom Domain). This is for customer support, not promotions.

**Not numeric stacking** - each feature is a toggle (has access / doesn't), not "+2 editors".

```prisma
model FeatureOverride {
  id         String    @id @default(cuid())
  dealerId   String
  dealer     Dealer    @relation(fields: [dealerId], references: [id], onDelete: Cascade)
  feature    String    // "custom_domain", "slider_editor", "quick_links", "lead_form"
  expiresAt  DateTime? // null = never expires
  grantedBy  String    // admin user ID
  grantedAt  DateTime  @default(now())
  reason     String?

  @@unique([dealerId, feature])
  @@index([dealerId])
  @@index([expiresAt])
}
```

### 2. Note Editability
**Decision:** Notes can be edited. Trusted admins.

Keep edit history via `updatedAt` + `updatedBy` fields for audit trail.

```prisma
model DealerNote {
  id        String   @id @default(cuid())
  dealerId  String
  dealer    Dealer   @relation(fields: [dealerId], references: [id], onDelete: Cascade)
  adminId   String
  admin     User     @relation(fields: [adminId], references: [id])
  content   String
  noteType  String   // "internal", "support_call", "email_sent", "status_change", "tier_change"
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  updatedBy String?

  @@index([dealerId])
  @@index([createdAt])
}
```

### 3. Bulk Operation Limits
**Decision:** 20+ dealers triggers enhanced confirmation for dangerous actions

Confirmation UI combines count warning + preview list:
- Shows: "You're about to suspend 47 dealers"
- Preview list: First 10 dealers + "and X more" (expandable)
- Requires typing confirmation: "Type SUSPEND 47 to confirm"

### 4. Dangerous Operation Warnings
**Decision:** Warning with prominent UI, not approval workflow

| Action | Dangerous? | Warning Text |
|--------|-----------|--------------|
| Suspend dealer | Yes | "This will disable the dealer's site" |
| Cancel dealer | Yes | "This will permanently terminate their subscription" |
| Downgrade tier | Yes | "Dealer may lose access to features" |
| Upgrade tier | No | (beneficial) |
| Change subdomain | Yes | "This will change their public URL" |
| Delete dealer | Very Yes | "This cannot be undone" |

### 5. Reason Field Format
**Decision:** Dropdown with common options + "Other" free text

Options:
- Policy violation
- Non-payment
- Customer request
- Partner promotion
- Bug compensation
- Other: [free text]

### 6. Tier Downgrade Handling
**Decision:** PENDING - Needs boss input

See [Tier Downgrade Decision Document](#tier-downgrade-decision-document) below.

### 7. Custom Domain Health Check Scope
**Decision:** Cloudflare API checks, phased rollout post-launch

Checks to implement:
- DNS record exists and points correctly
- SSL certificate status
- Zone active status

### 8. Dealer Merge (Future)
**Decision:** Defer to active sub, keep pages/leads/analytics/notes, admin decides editor info

Future expansion: wizard/workflow for admin to actively decide what to keep.

---

## Feasibility Findings

### Already Exists (Can Reuse)
| Feature | Location | Notes |
|---------|----------|-------|
| Optimistic locking | `app/api/checkout/upgrade/route.ts:256-284` | Uses `updateMany` with `updatedAt` check |
| Idempotency keys | `app/api/checkout/upgrade/route.ts:186-189` | State-based key generation |
| AdminAction model | `prisma/schema.prisma` | Has all fields except `status` |
| Modal components | `components/ui/Modal.tsx` | Supports small/default/wide sizes |
| Admin auth | `lib/admin-auth.ts` | `requireAdmin()`, `getRequestContext()` |

### Needs Implementation
| Feature | Issue |
|---------|-------|
| Webhook respects admin changes | Currently blindly overwrites tier |
| Admin tier change API | Only customer-initiated upgrades exist |
| Feature override checking | `lib/plan-features.ts` is marketing only |
| Status/tier badge interactivity | Currently static display |

### Critical Discovery: User Visibility Gap
The admin page only shows Dealers. Users can get "stuck" at multiple points:

| State | Identification | Count Query |
|-------|---------------|-------------|
| Email Pending | `PendingRegistration` exists | `SELECT * FROM PendingRegistration WHERE expiresAt > NOW()` |
| User, No Dealer | User exists, no Dealer record | `SELECT u.* FROM User u LEFT JOIN Dealer d ON u.id = d.userId WHERE d.id IS NULL` |
| Registration Pending | Dealer `status = 'registration_pending'` | `SELECT * FROM Dealer WHERE status = 'registration_pending'` |
| No Subdomain | Dealer exists, `subdomain = NULL` | `SELECT * FROM Dealer WHERE subdomain IS NULL` |
| Never Published | Dealer `lastPublishedAt = NULL` | `SELECT * FROM Dealer WHERE lastPublishedAt IS NULL` |

---

## Epic Structure

### Epic 0: Foundation
*Must complete before other epics*

| Issue | Title | Complexity |
|-------|-------|------------|
| 0.1 | Add optimistic locking to Dealer mutations | Easy (pattern exists) |
| 0.2 | Add idempotency key infrastructure for Stripe | Easy (pattern exists) |
| 0.3 | Update webhook handler to respect admin changes | Medium |
| 0.4 | Add FeatureOverride and DealerNote models | Easy |
| 0.5 | Add audit logging for failed admin actions | Easy |

### Epic 1: Status Management

| Issue | Title |
|-------|-------|
| 1.1 | Status change dropdown with confirmation modal |
| 1.2 | Bulk status change with confirmation |
| 1.3 | Dealer notification on status change |

### Epic 2: Tier & Feature Management

| Issue | Title |
|-------|-------|
| 2.1 | Tier change modal with Stripe integration |
| 2.2 | Feature override management modal |
| 2.3 | Feature access check utility |

### Epic 3: Domain Management

| Issue | Title |
|-------|-------|
| 3.1 | Domain settings panel (read-only) |
| 3.2 | Custom domain health check |

### Epic 4: Quick Actions & Notes

| Issue | Title |
|-------|-------|
| 4.1 | Expanded quick actions menu |
| 4.2 | Notes system - Add note modal |
| 4.3 | Notes system - View history panel |

### Epic 5: Dashboard Intelligence

| Issue | Title |
|-------|-------|
| 5.1 | Clickable stat cards that filter table |
| 5.2 | Alerts/flags section for issues needing attention |
| 5.3 | Recent admin activity feed |
| 5.4 | Enhanced search with ID support |

### Epic 6: Audit & Compliance

| Issue | Title |
|-------|-------|
| 6.1 | Audit log viewer page |
| 6.2 | Audit log export with streaming |

### Epic 7: User Management (NEW - High Priority)

| Issue | Title | Priority |
|-------|-------|----------|
| 7.1 | Users API endpoint | **P0** |
| 7.2 | Pending Registrations API | P1 |
| 7.3 | Admin Users tab | **P0** |
| 7.4 | Pending Registrations tab | P1 |
| 7.5 | User detail slide-out panel | P2 |

---

## Implementation Order

```
Phase 0 (Immediate - MVP Launch Need):
├── 7.1 Users API endpoint
└── 7.3 Admin Users tab (basic)

Phase 1 (Foundation):
├── 0.4 FeatureOverride + DealerNote models
├── 0.5 AdminAction status field
└── 0.3 Webhook handler fix

Phase 2 (Core Admin Features):
├── 1.1 Status change dropdown
├── 4.1 Quick actions menu
└── 5.1 Clickable stat cards

Phase 3 (Tier & Features):
├── 2.1 Tier change modal
├── 2.2 Feature override management
└── 7.2 + 7.4 Pending registrations

Phase 4 (Polish):
├── Remaining epic items
```

---

## Detailed Issue Specifications

### Issue 0.1: Add optimistic locking to Dealer mutations

**Description:**
Prevent race conditions when multiple admins edit the same dealer simultaneously.

**Acceptance Criteria:**
- [ ] All dealer mutation endpoints accept optional `expectedUpdatedAt` parameter
- [ ] If `expectedUpdatedAt` doesn't match current `updatedAt`, return 409 Conflict
- [ ] Conflict response includes current dealer state for client-side diff
- [ ] Admin UI shows conflict modal: "This dealer was modified by [admin] at [time]"

**Technical Notes:**
```typescript
const result = await prisma.dealer.updateMany({
  where: {
    id: dealerId,
    updatedAt: expectedUpdatedAt
  },
  data: { status: newStatus }
});
if (result.count === 0) throw new ConflictError();
```

---

### Issue 0.3: Update webhook handler to respect admin changes

**Description:**
Prevent Stripe webhooks from overwriting admin tier changes.

**Acceptance Criteria:**
- [ ] Add `lastModifiedBy` field to Dealer: `"admin" | "webhook" | "user" | "system"`
- [ ] Add `lastModifiedAt` field with millisecond precision
- [ ] Webhook checks: if `lastModifiedBy === "admin"` AND `lastModifiedAt` recent, skip update
- [ ] Log skipped webhook updates for debugging
- [ ] Admin changes set 30-second "cool-off" window

**Schema Addition:**
```prisma
model Dealer {
  // ... existing fields
  lastModifiedBy    String?
  lastModifiedAt    DateTime?
}
```

---

### Issue 0.4: Add FeatureOverride and DealerNote models

**Description:**
Add database models for feature overrides and admin notes.

**Acceptance Criteria:**
- [ ] `FeatureOverride` model created (see schema in Design Decisions)
- [ ] `DealerNote` model created (see schema in Design Decisions)
- [ ] Both have `onDelete: Cascade` for dealer relation
- [ ] Migration runs cleanly

---

### Issue 0.5: Add audit logging for failed admin actions

**Description:**
Log failed admin action attempts for compliance.

**Acceptance Criteria:**
- [ ] `AdminAction` model gains `status` field: `"success" | "failed" | "partial"`
- [ ] `AdminAction.details` includes `errorMessage` for failures
- [ ] All admin mutations log attempts before execution
- [ ] Bulk operations log partial success

---

### Issue 1.1: Status change dropdown with confirmation modal

**Description:**
Allow admins to change dealer status by clicking the status badge.

**Acceptance Criteria:**
- [ ] Clicking status badge opens dropdown with status options
- [ ] Selecting new status opens confirmation modal
- [ ] Modal shows: Current → New status, warning text for dangerous statuses
- [ ] Reason dropdown (see Design Decisions #5)
- [ ] Optional free-text notes field
- [ ] "Notify dealer" checkbox (unchecked by default)
- [ ] On success: Table refreshes, toast notification

**API Endpoint:** `PATCH /api/admin/dealers/[id]/status`

---

### Issue 2.1: Tier change modal with Stripe integration

**Description:**
Allow admins to change dealer subscription tier.

**Acceptance Criteria:**
- [ ] Clicking tier badge opens modal
- [ ] Modal shows: Current tier/price, tier selector, billing impact
- [ ] Proration options: "Apply immediately" / "Apply at next billing cycle"
- [ ] Reason dropdown + optional notes
- [ ] Idempotency key generated and passed to Stripe
- [ ] Warning for downgrades about feature loss

**API Endpoint:** `PATCH /api/admin/dealers/[id]/tier`

---

### Issue 2.2: Feature override management modal

**Description:**
Allow admins to grant specific premium features regardless of tier.

**Acceptance Criteria:**
- [ ] "Manage Features" in quick actions menu
- [ ] Modal shows: Current tier features, all possible features with checkboxes
- [ ] Active overrides shown with expiration and "Remove" button
- [ ] Add override form: Feature dropdown, Expiration (Never/Date), Reason
- [ ] Feature list: Custom Domain, Slider Editor, Quick Links, Lead Form, etc.

**API Endpoints:**
- `GET /api/admin/dealers/[id]/features`
- `POST /api/admin/dealers/[id]/features`
- `DELETE /api/admin/dealers/[id]/features/[featureId]`

---

### Issue 4.1: Expanded quick actions menu

**Description:**
Replace single "Support" button with full actions dropdown.

**Acceptance Criteria:**
- [ ] Actions column shows "..." button
- [ ] Dropdown menu structure:
  ```
  Support
  ├── Start Support Session
  ├── View Live Site
  ├── View in Stripe
  ─────────────
  Manage
  ├── Change Status
  ├── Change Tier
  ├── Manage Features
  ├── Domain Settings
  ─────────────
  Notes
  ├── Add Note
  ├── View History
  ─────────────
  Danger Zone
  └── Delete Dealer
  ```
- [ ] Keyboard accessible

---

### Issue 7.1: Users API endpoint

**Description:**
Create `/api/admin/users` to list all users with registration status.

**Acceptance Criteria:**
- [ ] GET endpoint with pagination
- [ ] Filters: status, date range, search (email, name)
- [ ] Joins User with Dealer and PendingRegistration
- [ ] Returns computed `status` field per user

**User Status Enum:**
```typescript
enum UserStatus {
  EMAIL_PENDING = 'email_pending',
  NO_DEALER = 'no_dealer',
  REGISTRATION_PENDING = 'registration_pending',
  INCOMPLETE_ONBOARDING = 'incomplete',
  ACTIVE = 'active',
  SUSPENDED = 'suspended',
  CANCELLED = 'cancelled',
  PAYMENT_FAILED = 'payment_failed'
}
```

---

### Issue 7.3: Admin Users tab

**Description:**
Add "Users" tab to admin dashboard.

**Acceptance Criteria:**
- [ ] Tab navigation: Dealers | Users
- [ ] Users table: Email, Name, Status (badge), Created, Dealer Status, Actions
- [ ] Status badges color-coded
- [ ] Search by email, name
- [ ] Filter by status
- [ ] Actions: View details, Impersonate (if has dealer), Delete

---

## Tier Downgrade Decision Document

*For forwarding to decision-maker*

---

**Subject: Decision Needed: How should admin tier downgrades handle existing features?**

**The Scenario:**

A dealer is on **Professional** tier and has:
- A custom domain configured (dealerbob.com)
- 5 editors with active accounts
- 10 published pages

An admin needs to downgrade them to **Starter** tier, which only allows:
- No custom domain
- 1 editor
- 3 pages

**The Question:** What should happen to features that exceed the new tier's limits?

**Options:**

| Option | Behavior | Pros | Cons |
|--------|----------|------|------|
| **A) Block** | Cannot downgrade until features manually removed | Prevents accidental data loss | More work for admin |
| **B) Warn + Disable immediately** | Show warning, on confirm: disable features | Clean, immediate | Could break dealer unexpectedly |
| **C) Warn + Grace period** | Features remain 7 days, then disabled | Dealer has time to react | More complex |
| **D) Warn + Auto-override** | Downgrade tier but auto-create feature overrides | No disruption | Defeats tier limits |

**Recommendation:** Option **B** or **C** depending on context:
- Punitive downgrade (non-payment): **Option B**
- Customer support gesture: **Option C**

Could also be admin's choice in the UI: "Disable features immediately / Give 7-day grace period"

---

## Open Questions

1. **Tier downgrade handling** - Awaiting decision (see above)
2. **Subdomain editing** - Cloudflare integration details (now using Cloudflare DNS generation)
3. **Email notification templates** - Need design for status change emails

---

## Related Files

- `/app/admin/page.tsx` - Main admin dashboard
- `/components/admin/DealerTable.tsx` - Dealer table component
- `/lib/admin-auth.ts` - Admin authentication utilities
- `/lib/plan-features.ts` - Tier feature definitions (marketing)
- `/app/api/admin/dealers/route.ts` - Dealers API
- `/app/api/webhooks/stripe/route.ts` - Stripe webhook handler
- `/prisma/schema.prisma` - Database schema
