# Dealer Detail Modal + Activity/Notes System

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Create a comprehensive dealer detail modal that opens when clicking a row in the admin table, showing all dealer information with an integrated activity/notes timeline that combines manual admin notes with auto-logged system events.

**Architecture:** Click dealer row → Large modal with dealer info sections → Activity timeline at bottom showing manual notes + auto-logged events (impersonation, subdomain changes, status changes, etc.)

**Tech Stack:** Next.js 16 App Router, React 19, Prisma 7, TypeScript

---

## Task 1: Add DealerNote Model

**Files:**

- Modify: `prisma/schema.prisma`

**Step 1: Add the DealerNote model**

```prisma
model DealerNote {
  id          String   @id @default(cuid())
  dealerId    String
  dealer      Dealer   @relation(fields: [dealerId], references: [id], onDelete: Cascade)
  adminId     String
  admin       User     @relation("DealerNotes", fields: [adminId], references: [id])
  content     String
  noteType    String   // "manual" | "support_call" | "email_sent" | "internal"
  isAutomatic Boolean  @default(false)
  metadata    Json?    // For auto-notes: store before/after values
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  updatedBy   String?

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

**Step 2: Add relation to Dealer model**

Add to Dealer model:

```prisma
notes            DealerNote[]
```

**Step 3: Add relation to User model**

Add to User model:

```prisma
dealerNotes      DealerNote[] @relation("DealerNotes")
```

**Step 4: Sync database**

```bash
npm run sync-db
```

**Verification:**

- [ ] `npx prisma validate` passes
- [ ] `npm run sync-db` completes without errors
- [ ] Can query `prisma.dealerNote.findMany()` in a test

---

## Task 2: Create Activity API Endpoint

**Files:**

- Create: `app/api/admin/dealers/[id]/activity/route.ts`

**Purpose:** Fetch combined timeline of manual notes + auto-logged AdminAction events for a dealer.

**Step 1: Create the GET endpoint**

```typescript
// app/api/admin/dealers/[id]/activity/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/admin-auth';
import { prisma } from '@/lib/prisma';

interface ActivityItem {
  id: string;
  type: 'note' | 'action';
  timestamp: Date;
  adminId: string;
  adminName: string | null;
  adminEmail: string;
  // For notes
  noteType?: string;
  content?: string;
  isEditable?: boolean;
  // For actions
  action?: string;
  details?: Record<string, unknown>;
  reason?: string;
}

export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const admin = await requireAdmin();
  if (!admin) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { id: dealerId } = await params;

  // Fetch notes
  const notes = await prisma.dealerNote.findMany({
    where: { dealerId },
    include: { admin: { select: { id: true, name: true, email: true } } },
    orderBy: { createdAt: 'desc' },
  });

  // Fetch AdminAction entries for this dealer
  const actions = await prisma.adminAction.findMany({
    where: { targetType: 'dealer', targetId: dealerId },
    include: { adminUser: { select: { id: true, name: true, email: true } } },
    orderBy: { createdAt: 'desc' },
  });

  // Combine and sort by timestamp
  const activity: ActivityItem[] = [
    ...notes.map((note) => ({
      id: note.id,
      type: 'note' as const,
      timestamp: note.createdAt,
      adminId: note.adminId,
      adminName: note.admin.name,
      adminEmail: note.admin.email,
      noteType: note.noteType,
      content: note.content,
      isEditable: !note.isAutomatic,
    })),
    ...actions.map((action) => ({
      id: action.id,
      type: 'action' as const,
      timestamp: action.createdAt,
      adminId: action.adminUserId,
      adminName: action.adminUser.name,
      adminEmail: action.adminUser.email,
      action: action.action,
      details: action.details as Record<string, unknown>,
      reason: action.reason || undefined,
    })),
  ].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());

  return NextResponse.json({ activity });
}
```

**Verification:**

- [ ] GET `/api/admin/dealers/[id]/activity` returns combined timeline
- [ ] Results sorted by timestamp descending (newest first)
- [ ] Includes admin name/email for attribution

---

## Task 3: Create Notes CRUD Endpoints

**Files:**

- Create: `app/api/admin/dealers/[id]/notes/route.ts`
- Create: `app/api/admin/dealers/[id]/notes/[noteId]/route.ts`

**Step 1: Create POST endpoint for adding notes**

```typescript
// app/api/admin/dealers/[id]/notes/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin, getRequestContext } from '@/lib/admin-auth';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';

const createNoteSchema = z.object({
  content: z.string().min(1).max(5000),
  noteType: z.enum(['manual', 'support_call', 'email_sent', 'internal']),
});

export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const admin = await requireAdmin();
  if (!admin) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { id: dealerId } = await params;
  const body = await request.json();

  const validation = createNoteSchema.safeParse(body);
  if (!validation.success) {
    return NextResponse.json(
      { error: 'Invalid input', details: validation.error.errors },
      { status: 400 }
    );
  }

  const { content, noteType } = validation.data;

  // Verify dealer exists
  const dealer = await prisma.dealer.findUnique({ where: { id: dealerId } });
  if (!dealer) {
    return NextResponse.json({ error: 'Dealer not found' }, { status: 404 });
  }

  // Create the note
  const note = await prisma.dealerNote.create({
    data: {
      dealerId,
      adminId: admin.id,
      content,
      noteType,
      isAutomatic: false,
    },
    include: { admin: { select: { id: true, name: true, email: true } } },
  });

  // Log the action
  const ctx = await getRequestContext(request);
  await prisma.adminAction.create({
    data: {
      adminUserId: admin.id,
      action: 'note_added',
      targetType: 'dealer',
      targetId: dealerId,
      details: { noteId: note.id, noteType },
      ipAddress: ctx.ipAddress,
      userAgent: ctx.userAgent,
    },
  });

  return NextResponse.json({ note }, { status: 201 });
}
```

**Step 2: Create PATCH/DELETE endpoint for editing/deleting notes**

```typescript
// app/api/admin/dealers/[id]/notes/[noteId]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin, getRequestContext } from '@/lib/admin-auth';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';

const updateNoteSchema = z.object({
  content: z.string().min(1).max(5000),
});

export async function PATCH(
  request: NextRequest,
  { params }: { params: Promise<{ id: string; noteId: string }> }
) {
  const admin = await requireAdmin();
  if (!admin) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { id: dealerId, noteId } = await params;
  const body = await request.json();

  const validation = updateNoteSchema.safeParse(body);
  if (!validation.success) {
    return NextResponse.json({ error: 'Invalid input' }, { status: 400 });
  }

  // Find the note
  const note = await prisma.dealerNote.findFirst({
    where: { id: noteId, dealerId },
  });

  if (!note) {
    return NextResponse.json({ error: 'Note not found' }, { status: 404 });
  }

  // Can't edit automatic notes
  if (note.isAutomatic) {
    return NextResponse.json({ error: 'Cannot edit automatic notes' }, { status: 403 });
  }

  // Update the note
  const updated = await prisma.dealerNote.update({
    where: { id: noteId },
    data: {
      content: validation.data.content,
      updatedBy: admin.id,
    },
    include: { admin: { select: { id: true, name: true, email: true } } },
  });

  return NextResponse.json({ note: updated });
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string; noteId: string }> }
) {
  const admin = await requireAdmin();
  if (!admin) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { id: dealerId, noteId } = await params;

  const note = await prisma.dealerNote.findFirst({
    where: { id: noteId, dealerId },
  });

  if (!note) {
    return NextResponse.json({ error: 'Note not found' }, { status: 404 });
  }

  if (note.isAutomatic) {
    return NextResponse.json({ error: 'Cannot delete automatic notes' }, { status: 403 });
  }

  await prisma.dealerNote.delete({ where: { id: noteId } });

  // Log deletion
  const ctx = await getRequestContext(request);
  await prisma.adminAction.create({
    data: {
      adminUserId: admin.id,
      action: 'note_deleted',
      targetType: 'dealer',
      targetId: dealerId,
      details: { noteId, noteType: note.noteType },
      ipAddress: ctx.ipAddress,
      userAgent: ctx.userAgent,
    },
  });

  return NextResponse.json({ success: true });
}
```

**Verification:**

- [ ] POST creates a new note
- [ ] PATCH updates note content
- [ ] DELETE removes a note
- [ ] Cannot edit/delete automatic notes (403)

---

## Task 4: Create Dealer Detail API Endpoint

**Files:**

- Create: `app/api/admin/dealers/[id]/detail/route.ts`

**Purpose:** Fetch comprehensive dealer information for the detail modal.

```typescript
// app/api/admin/dealers/[id]/detail/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { requireAdmin } from '@/lib/admin-auth';
import { prisma } from '@/lib/prisma';

export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const admin = await requireAdmin();
  if (!admin) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { id: dealerId } = await params;

  const dealer = await prisma.dealer.findUnique({
    where: { id: dealerId },
    include: {
      user: {
        select: {
          id: true,
          email: true,
          name: true,
          image: true,
          role: true,
          createdAt: true,
        },
      },
      _count: {
        select: {
          pages: true,
          leads: true,
          pageViews: true,
          navigationItems: true,
        },
      },
    },
  });

  if (!dealer) {
    return NextResponse.json({ error: 'Dealer not found' }, { status: 404 });
  }

  // Get recent activity count
  const recentActivityCount = await prisma.adminAction.count({
    where: {
      targetType: 'dealer',
      targetId: dealerId,
      createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
    },
  });

  // Get notes count
  const notesCount = await prisma.dealerNote.count({
    where: { dealerId },
  });

  return NextResponse.json({
    dealer: {
      ...dealer,
      stats: {
        pages: dealer._count.pages,
        leads: dealer._count.leads,
        pageViews: dealer._count.pageViews,
        navigationItems: dealer._count.navigationItems,
        recentActivity: recentActivityCount,
        notes: notesCount,
      },
    },
  });
}
```

**Verification:**

- [ ] Returns comprehensive dealer data
- [ ] Includes user info, counts, stats

---

## Task 5: Create DealerDetailModal Component

**Files:**

- Create: `components/admin/DealerDetailModal.tsx`
- Create: `components/admin/DealerDetailModal.module.css`

**Step 1: Create the modal component structure**

The modal should have these sections:

1. **Header**: Business name, status badge, tier badge, close button
2. **Basic Info Grid**: Contact details, ZO#, subdomain, created date
3. **Subscription Section**: Tier, Stripe links, subscription dates
4. **Stats Section**: Pages, leads, page views
5. **Activity & Notes Section**: Timeline with manual notes + auto-logged events

```typescript
// components/admin/DealerDetailModal.tsx
'use client';

import React, { useState, useEffect, useCallback } from 'react';
import { Modal } from '@/components/ui/Modal';
import styles from './DealerDetailModal.module.css';

interface DealerDetail {
  id: string;
  businessName: string | null;
  subdomain: string | null;
  domain: string;
  domainPrefix: string;
  customDomain: string | null;
  customDomainStatus: string;
  subscriptionTier: string;
  status: string;
  dealerNumber: string | null;
  phone: string;
  address: string;
  city: string;
  state: string;
  zip: string;
  country: string;
  contactName: string | null;
  contactEmail: string | null;
  stripeCustomerId: string;
  stripeSubscriptionId: string | null;
  hasLeadForm: boolean;
  createdAt: string;
  updatedAt: string;
  lastPublishedAt: string | null;
  user: {
    id: string;
    email: string;
    name: string | null;
    role: string;
    createdAt: string;
  };
  stats: {
    pages: number;
    leads: number;
    pageViews: number;
    notes: number;
    recentActivity: number;
  };
}

interface ActivityItem {
  id: string;
  type: 'note' | 'action';
  timestamp: string;
  adminId: string;
  adminName: string | null;
  adminEmail: string;
  noteType?: string;
  content?: string;
  isEditable?: boolean;
  action?: string;
  details?: Record<string, unknown>;
  reason?: string;
}

interface DealerDetailModalProps {
  isOpen: boolean;
  onClose: () => void;
  dealerId: string | null;
}

const TIER_COLORS: Record<string, string> = {
  starter: '#6b7280',
  growth: '#3b82f6',
  enhanced: '#8b5cf6',
  professional: '#f59e0b',
};

const STATUS_COLORS: Record<string, string> = {
  active: '#10b981',
  pending: '#f59e0b',
  suspended: '#ef4444',
  cancelled: '#6b7280',
  payment_failed: '#ef4444',
};

const ACTION_ICONS: Record<string, string> = {
  impersonation_start: '🔐',
  impersonation_end: '🔓',
  subdomain_change: '🌐',
  status_change: '📊',
  tier_change: '💳',
  note_added: '📝',
  note_deleted: '🗑️',
  dns_update: '🔗',
};

const NOTE_TYPE_LABELS: Record<string, string> = {
  manual: 'Note',
  support_call: 'Support Call',
  email_sent: 'Email Sent',
  internal: 'Internal Note',
};

export function DealerDetailModal({ isOpen, onClose, dealerId }: DealerDetailModalProps) {
  const [dealer, setDealer] = useState<DealerDetail | null>(null);
  const [activity, setActivity] = useState<ActivityItem[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [activeTab, setActiveTab] = useState<'info' | 'activity'>('info');
  const [newNote, setNewNote] = useState('');
  const [noteType, setNoteType] = useState<string>('manual');
  const [isAddingNote, setIsAddingNote] = useState(false);

  const fetchDealerDetail = useCallback(async () => {
    if (!dealerId) return;
    setIsLoading(true);
    try {
      const [detailRes, activityRes] = await Promise.all([
        fetch(`/api/admin/dealers/${dealerId}/detail`),
        fetch(`/api/admin/dealers/${dealerId}/activity`),
      ]);

      if (detailRes.ok) {
        const data = await detailRes.json();
        setDealer(data.dealer);
      }
      if (activityRes.ok) {
        const data = await activityRes.json();
        setActivity(data.activity);
      }
    } catch (error) {
      console.error('Failed to fetch dealer details:', error);
    } finally {
      setIsLoading(false);
    }
  }, [dealerId]);

  useEffect(() => {
    if (isOpen && dealerId) {
      fetchDealerDetail();
    }
  }, [isOpen, dealerId, fetchDealerDetail]);

  const handleAddNote = async () => {
    if (!dealerId || !newNote.trim()) return;
    setIsAddingNote(true);
    try {
      const res = await fetch(`/api/admin/dealers/${dealerId}/notes`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: newNote, noteType }),
      });
      if (res.ok) {
        setNewNote('');
        fetchDealerDetail(); // Refresh activity
      }
    } catch (error) {
      console.error('Failed to add note:', error);
    } finally {
      setIsAddingNote(false);
    }
  };

  const formatDate = (dateStr: string) => {
    return new Date(dateStr).toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
    });
  };

  const formatDateTime = (dateStr: string) => {
    return new Date(dateStr).toLocaleString('en-US', {
      month: 'short',
      day: 'numeric',
      hour: 'numeric',
      minute: '2-digit',
    });
  };

  const getActionDescription = (item: ActivityItem): string => {
    if (!item.action) return '';
    const details = item.details || {};
    switch (item.action) {
      case 'impersonation_start':
        return 'Started support session';
      case 'impersonation_end':
        return 'Ended support session';
      case 'subdomain_change':
        return `Changed subdomain: ${details.oldSubdomain || 'none'} → ${details.newSubdomain}`;
      case 'status_change':
        return `Changed status: ${details.oldStatus} → ${details.newStatus}`;
      case 'tier_change':
        return `Changed tier: ${details.oldTier} → ${details.newTier}`;
      case 'dns_update':
        return 'Updated DNS records';
      default:
        return item.action.replace(/_/g, ' ');
    }
  };

  if (!isOpen) return null;

  return (
    <Modal isOpen={isOpen} onClose={onClose} size="wide">
      <div className={styles.container}>
        {isLoading ? (
          <div className={styles.loading}>Loading dealer details...</div>
        ) : dealer ? (
          <>
            {/* Header */}
            <div className={styles.header}>
              <div className={styles.headerLeft}>
                <h2 className={styles.title}>
                  {dealer.businessName || dealer.user.name || 'Unnamed Dealer'}
                </h2>
                <div className={styles.badges}>
                  <span
                    className={styles.tierBadge}
                    style={{ backgroundColor: TIER_COLORS[dealer.subscriptionTier] }}
                  >
                    {dealer.subscriptionTier}
                  </span>
                  <span
                    className={styles.statusBadge}
                    style={{ borderColor: STATUS_COLORS[dealer.status] }}
                  >
                    <span
                      className={styles.statusDot}
                      style={{ backgroundColor: STATUS_COLORS[dealer.status] }}
                    />
                    {dealer.status.replace('_', ' ')}
                  </span>
                </div>
              </div>
              <button className={styles.closeButton} onClick={onClose}>
                ×
              </button>
            </div>

            {/* Tabs */}
            <div className={styles.tabs}>
              <button
                className={`${styles.tab} ${activeTab === 'info' ? styles.activeTab : ''}`}
                onClick={() => setActiveTab('info')}
              >
                Details
              </button>
              <button
                className={`${styles.tab} ${activeTab === 'activity' ? styles.activeTab : ''}`}
                onClick={() => setActiveTab('activity')}
              >
                Activity & Notes ({dealer.stats.notes + dealer.stats.recentActivity})
              </button>
            </div>

            {/* Content */}
            <div className={styles.content}>
              {activeTab === 'info' ? (
                <div className={styles.infoGrid}>
                  {/* Contact Info */}
                  <section className={styles.section}>
                    <h3 className={styles.sectionTitle}>Contact Information</h3>
                    <dl className={styles.infoList}>
                      <div className={styles.infoItem}>
                        <dt>Email</dt>
                        <dd>{dealer.user.email}</dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Contact Name</dt>
                        <dd>{dealer.contactName || dealer.user.name || '—'}</dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Phone</dt>
                        <dd>{dealer.phone || '—'}</dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>ZO Number</dt>
                        <dd>{dealer.dealerNumber ? `#${dealer.dealerNumber}` : '—'}</dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Address</dt>
                        <dd>
                          {dealer.address ? (
                            <>
                              {dealer.address}<br />
                              {dealer.city}, {dealer.state} {dealer.zip}<br />
                              {dealer.country}
                            </>
                          ) : '—'}
                        </dd>
                      </div>
                    </dl>
                  </section>

                  {/* Site Info */}
                  <section className={styles.section}>
                    <h3 className={styles.sectionTitle}>Site Information</h3>
                    <dl className={styles.infoList}>
                      <div className={styles.infoItem}>
                        <dt>Subdomain</dt>
                        <dd>
                          {dealer.subdomain ? (
                            <a
                              href={`https://${dealer.subdomain}.${dealer.domainPrefix}.${dealer.domain === 'ca' ? 'ca' : 'com'}`}
                              target="_blank"
                              rel="noopener noreferrer"
                              className={styles.link}
                            >
                              {dealer.subdomain}.{dealer.domainPrefix}.{dealer.domain === 'ca' ? 'ca' : 'com'}
                            </a>
                          ) : (
                            <span className={styles.notSet}>Not set</span>
                          )}
                        </dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Custom Domain</dt>
                        <dd>
                          {dealer.customDomain ? (
                            <>
                              {dealer.customDomain}
                              <span className={styles.domainStatus}>
                                ({dealer.customDomainStatus})
                              </span>
                            </>
                          ) : (
                            <span className={styles.notSet}>None</span>
                          )}
                        </dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Last Published</dt>
                        <dd>
                          {dealer.lastPublishedAt
                            ? formatDate(dealer.lastPublishedAt)
                            : <span className={styles.notSet}>Never</span>}
                        </dd>
                      </div>
                    </dl>
                  </section>

                  {/* Subscription Info */}
                  <section className={styles.section}>
                    <h3 className={styles.sectionTitle}>Subscription</h3>
                    <dl className={styles.infoList}>
                      <div className={styles.infoItem}>
                        <dt>Tier</dt>
                        <dd className={styles.capitalize}>{dealer.subscriptionTier}</dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Stripe Customer</dt>
                        <dd>
                          <a
                            href={`https://dashboard.stripe.com/customers/${dealer.stripeCustomerId}`}
                            target="_blank"
                            rel="noopener noreferrer"
                            className={styles.link}
                          >
                            {dealer.stripeCustomerId}
                          </a>
                        </dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Member Since</dt>
                        <dd>{formatDate(dealer.createdAt)}</dd>
                      </div>
                      <div className={styles.infoItem}>
                        <dt>Lead Form</dt>
                        <dd>{dealer.hasLeadForm ? '✓ Enabled' : '✗ Disabled'}</dd>
                      </div>
                    </dl>
                  </section>

                  {/* Stats */}
                  <section className={styles.section}>
                    <h3 className={styles.sectionTitle}>Stats</h3>
                    <div className={styles.statsGrid}>
                      <div className={styles.statCard}>
                        <div className={styles.statValue}>{dealer.stats.pages}</div>
                        <div className={styles.statLabel}>Pages</div>
                      </div>
                      <div className={styles.statCard}>
                        <div className={styles.statValue}>{dealer.stats.leads}</div>
                        <div className={styles.statLabel}>Leads</div>
                      </div>
                      <div className={styles.statCard}>
                        <div className={styles.statValue}>{dealer.stats.pageViews}</div>
                        <div className={styles.statLabel}>Page Views</div>
                      </div>
                    </div>
                  </section>
                </div>
              ) : (
                <div className={styles.activitySection}>
                  {/* Add Note Form */}
                  <div className={styles.addNoteForm}>
                    <div className={styles.noteTypeSelect}>
                      <select
                        value={noteType}
                        onChange={(e) => setNoteType(e.target.value)}
                        className={styles.select}
                      >
                        <option value="manual">Note</option>
                        <option value="support_call">Support Call</option>
                        <option value="email_sent">Email Sent</option>
                        <option value="internal">Internal</option>
                      </select>
                    </div>
                    <textarea
                      value={newNote}
                      onChange={(e) => setNewNote(e.target.value)}
                      placeholder="Add a note..."
                      className={styles.noteInput}
                      rows={2}
                    />
                    <button
                      onClick={handleAddNote}
                      disabled={isAddingNote || !newNote.trim()}
                      className={styles.addNoteButton}
                    >
                      {isAddingNote ? 'Adding...' : 'Add Note'}
                    </button>
                  </div>

                  {/* Activity Timeline */}
                  <div className={styles.timeline}>
                    {activity.length === 0 ? (
                      <div className={styles.emptyActivity}>
                        No activity yet
                      </div>
                    ) : (
                      activity.map((item) => (
                        <div
                          key={item.id}
                          className={`${styles.timelineItem} ${
                            item.type === 'note' ? styles.noteItem : styles.actionItem
                          }`}
                        >
                          <div className={styles.timelineIcon}>
                            {item.type === 'note'
                              ? '📝'
                              : ACTION_ICONS[item.action || ''] || '•'}
                          </div>
                          <div className={styles.timelineContent}>
                            <div className={styles.timelineHeader}>
                              <span className={styles.timelineTime}>
                                {formatDateTime(item.timestamp)}
                              </span>
                              {item.type === 'note' && item.noteType && (
                                <span className={styles.noteTypeTag}>
                                  {NOTE_TYPE_LABELS[item.noteType] || item.noteType}
                                </span>
                              )}
                              {item.type === 'action' && (
                                <span className={styles.autoTag}>auto</span>
                              )}
                            </div>
                            <div className={styles.timelineBody}>
                              {item.type === 'note' ? (
                                <p>{item.content}</p>
                              ) : (
                                <p>{getActionDescription(item)}</p>
                              )}
                            </div>
                            <div className={styles.timelineFooter}>
                              by {item.adminName || item.adminEmail}
                            </div>
                          </div>
                        </div>
                      ))
                    )}
                  </div>
                </div>
              )}
            </div>
          </>
        ) : (
          <div className={styles.error}>Failed to load dealer details</div>
        )}
      </div>
    </Modal>
  );
}
```

**Step 2: Create the CSS module**

Create comprehensive styles for the modal with:

- Two-column info grid layout
- Timeline styling with icons
- Note input form
- Responsive design

(CSS file will be ~200 lines - basic layout, colors matching existing admin styles)

**Verification:**

- [ ] Modal opens and displays dealer info
- [ ] Tabs switch between Details and Activity
- [ ] Activity timeline shows formatted entries
- [ ] Can add new notes

---

## Task 6: Integrate Modal into DealerTable

**Files:**

- Modify: `components/admin/DealerTable.tsx`

**Step 1: Add state for selected dealer**

```typescript
const [selectedDealerId, setSelectedDealerId] = useState<string | null>(null);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
```

**Step 2: Make rows clickable**

Update the `<tr>` element:

```tsx
<tr
  key={dealer.id}
  onClick={() => {
    setSelectedDealerId(dealer.id);
    setIsDetailModalOpen(true);
  }}
  className={styles.clickableRow}
>
```

**Step 3: Add modal to component**

```tsx
<DealerDetailModal
  isOpen={isDetailModalOpen}
  onClose={() => {
    setIsDetailModalOpen(false);
    setSelectedDealerId(null);
  }}
  dealerId={selectedDealerId}
/>
```

**Step 4: Add clickable row styles**

```css
.clickableRow {
  cursor: pointer;
  transition: background-color 0.15s;
}

.clickableRow:hover {
  background-color: rgba(59, 130, 246, 0.05);
}
```

**Step 5: Prevent row click when clicking action buttons**

Add `onClick={(e) => e.stopPropagation()}` to interactive elements in cells (QuickActionsMenu, subdomain edit button, external link).

**Verification:**

- [ ] Clicking a row opens the detail modal
- [ ] Clicking quick actions menu doesn't open modal
- [ ] Clicking subdomain edit doesn't open modal
- [ ] Clicking external link doesn't open modal

---

## Task 7: Add Auto-Logging to Existing Actions

**Files:**

- Modify: `app/api/admin/impersonate/route.ts` (or wherever impersonation is handled)
- Modify: `app/api/admin/dealers/[id]/subdomain/route.ts`
- Modify: `app/api/admin/dealers/[id]/status/route.ts` (if exists from status change PR)

**Purpose:** Ensure significant admin actions create AdminAction records with details that the activity timeline can display.

**Step 1: Verify impersonation logging**

Check if impersonation already logs to AdminAction. If not, add:

```typescript
await prisma.adminAction.create({
  data: {
    adminUserId: admin.id,
    action: 'impersonation_start',
    targetType: 'dealer',
    targetId: dealer.id,
    details: { dealerEmail: dealer.user.email },
    ipAddress: ctx.ipAddress,
    userAgent: ctx.userAgent,
  },
});
```

**Step 2: Verify subdomain change logging**

The subdomain endpoint should already log, but verify details include `oldSubdomain` and `newSubdomain`:

```typescript
details: {
  oldSubdomain: dealer.subdomain,
  newSubdomain: newSubdomain,
  domain: domain,
}
```

**Step 3: Verify status change logging**

If status change from PR #289 exists, verify it logs with `oldStatus` and `newStatus` in details.

**Verification:**

- [ ] Impersonation creates AdminAction with `impersonation_start`
- [ ] Subdomain change logs old/new values
- [ ] Status change logs old/new values
- [ ] All logged actions appear in dealer activity timeline

---

## Task 8: Manual Testing & Polish

**Testing Checklist:**

- [ ] Click dealer row → Modal opens with correct data
- [ ] Switch between Details and Activity tabs
- [ ] All dealer info displays correctly (handle nulls gracefully)
- [ ] Stripe links open correct dashboard pages
- [ ] Add a manual note → Appears in timeline
- [ ] Edit a note → Content updates
- [ ] Auto-logged actions appear (impersonation, subdomain change)
- [ ] Timeline sorted newest-first
- [ ] Modal closes on X button
- [ ] Modal closes on backdrop click
- [ ] Quick actions menu still works (doesn't trigger row click)
- [ ] External link icon still works

**Polish:**

- [ ] Loading states display properly
- [ ] Empty states display properly
- [ ] Error states handled gracefully
- [ ] Responsive on smaller screens

---

## Task 9: Commit and Push

```bash
git add .
git commit -m "feat(admin): add dealer detail modal with activity/notes system

- Add DealerNote model for manual admin notes
- Add detail modal showing comprehensive dealer info
- Add activity timeline combining notes + auto-logged events
- Click dealer row to open detail modal
- Support adding/editing manual notes
- Auto-log impersonation, subdomain changes, status changes"

git push -u origin feature/dealer-detail-modal
```

Then create PR:

```bash
gh pr create --title "Dealer Detail Modal with Activity & Notes" --body "## Summary
- Add comprehensive dealer detail modal accessible by clicking table rows
- Integrated activity timeline showing manual notes + auto-logged events
- Support for adding/editing admin notes with type classification

## Features
- **Dealer Info**: Contact details, subscription info, site info, stats
- **Activity Timeline**: Chronological view of all dealer-related events
- **Manual Notes**: Support call logs, emails sent, internal notes
- **Auto Logging**: Impersonation, subdomain changes, status changes

## Test Plan
- [ ] Click dealer row to open modal
- [ ] Verify all dealer info displays correctly
- [ ] Add a manual note and verify it appears
- [ ] Perform an impersonation and verify it logs
- [ ] Change a subdomain and verify it logs

🤖 Generated with [Claude Code](https://claude.com/claude-code)" --base dev
```

---

## Summary

| Task | Description                 | Files                                          |
| ---- | --------------------------- | ---------------------------------------------- |
| 1    | Add DealerNote model        | `prisma/schema.prisma`                         |
| 2    | Activity API endpoint       | `app/api/admin/dealers/[id]/activity/route.ts` |
| 3    | Notes CRUD endpoints        | `app/api/admin/dealers/[id]/notes/...`         |
| 4    | Dealer detail API           | `app/api/admin/dealers/[id]/detail/route.ts`   |
| 5    | DealerDetailModal component | `components/admin/DealerDetailModal.tsx`       |
| 6    | Integrate into DealerTable  | `components/admin/DealerTable.tsx`             |
| 7    | Add auto-logging            | Various API routes                             |
| 8    | Manual testing              | -                                              |
| 9    | Commit and PR               | -                                              |
