1 OAK MLS

Admin UI

Internal admin app for managing workspaces, MLS connections, and sync operations

Admin UI Specification

Internal admin app for managing workspaces, MLS connections, and sync operations.

Overview

The admin app is an internal tool for:

  • Onboarding new clients (workspaces)
  • Configuring MLS connections
  • Managing field mappings
  • Monitoring sync operations
  • Configuring compliance text

Access: Internal team only (authenticated via Supabase Auth, role = admin)

Route Structure

/admin
├── /login                    # Auth
├── /workspaces               # List all workspaces
├── /workspaces/new           # Create workspace
└── /workspaces/[id]          # Workspace detail
    ├── /overview             # Dashboard (default)
    ├── /settings             # Site settings (agent, brokerage, theme)
    ├── /service-areas        # Service area management
    ├── /imports              # Import task management
    ├── /photos               # Photo management
    ├── /mls                  # MLS connection config
    ├── /mapping              # Field mapping editor
    ├── /search               # Search config (facets, sorts)
    ├── /compliance           # Attribution/disclaimer
    └── /sync                 # Sync monitor

Screens

1. Login (/admin/login)

Simple auth screen using Supabase Auth.

Components:

  • Email/password form
  • "Sign in" button
  • Error display

Flow:

  1. User enters credentials
  2. Call supabase.auth.signInWithPassword()
  3. On success, check user_profiles.role === 'admin'
  4. Redirect to /admin/workspaces

2. Workspace List (/admin/workspaces)

Grid/table of all workspaces.

Columns:

ColumnDescription
NameWorkspace name (link to detail)
SlugURL slug
Statusactive/paused/archived (badge)
DomainsPrimary domain
ListingsCount of active listings
Last SyncTimestamp + status indicator
ActionsEdit, Pause, Archive

Actions:

  • "New Workspace" button → /admin/workspaces/new
  • Row click → /admin/workspaces/[id]/overview
  • Quick status toggle (active ↔ paused)

Data Loading:

const workspaces = await supabase
  .from('workspaces')
  .select(`
    *,
    workspace_domains(domain, is_primary),
    mls_connections(status, last_synced_at),
    listings(count)
  `)
  .order('created_at', { ascending: false });

3. Create Workspace (/admin/workspaces/new)

Form to create new workspace.

Fields:

FieldTypeRequiredNotes
NametextYesDisplay name
SlugtextYesAuto-generated from name, editable
Primary DomaintextNoe.g., laineylevin.com

On Submit:

  1. Create workspaces row
  2. Create workspace_domains row (if domain provided)
  3. Create compliance_profiles row with defaults
  4. Redirect to /admin/workspaces/[id]/mls

4. Site Settings (/admin/workspaces/[id]/settings)

Configure workspace branding and site customization.

Sections:

Agent Profile

  • Name, title, email, phone
  • Photo upload
  • License number
  • Bio (Markdown supported)

Brokerage Info

  • Brokerage name
  • Logo upload
  • Address, phone, website
  • Instagram, Facebook, LinkedIn
  • Twitter, YouTube, TikTok

Site Customization

  • Hero image URL
  • Hero tagline
  • Featured listing selector
  • "Show All Listings" toggle

Theme

  • Preset selector (luxury, modern, coastal, classic)
  • Color overrides (optional)

Form with Change Detection: The settings form tracks changes and prompts before navigating away with unsaved edits.

4a. Service Areas (/admin/workspaces/[id]/service-areas)

Manage geographic service areas.

Service Area List:

ColumnDescription
NameArea name (link to edit)
SlugURL slug
ListingsCount of matching listings
Import StatusHas linked import task
OrderDisplay order
ActionsEdit, Delete, Create Import

Create/Edit Form:

  • Name, slug, tagline
  • Description (rich text)
  • Hero image URL
  • Filter configuration:
    • Cities (multi-select)
    • Subdivisions (multi-select)
    • Postal codes
    • Building names
    • Price range
    • Property types

Actions:

  • "Create Import Task" → Creates import task from area filter
  • Reorder via drag-and-drop

4b. Import Tasks (/admin/workspaces/[id]/imports)

Manage listing import tasks.

Import Task List:

ColumnDescription
NameTask name (link to edit)
Categorypersonal/office/search/custom (badge)
StatusEnabled/Disabled
ListingsCount of imported listings
Last SyncTimestamp + status
ActionsRun, Toggle, Edit, Delete

Create/Edit Form:

  • Name, description
  • Category selector
  • Filter configuration (varies by category)
  • Status filters (Active, Pending, etc.)
  • Price range
  • Max import limit
  • Sync cadence (manual/hourly/daily)
  • Visibility toggles

Actions:

  • "Run Sync" → Triggers immediate sync
  • Enable/Disable toggle
  • View sync history

4c. Photo Management (/admin/workspaces/[id]/photos)

Manage photos for workspace listings.

Listing Selector:

  • Search/filter listings
  • Show photo count per listing

Photo Grid (for selected listing):

  • Drag-and-drop reordering
  • Primary photo indicator
  • Upload new photos
  • Delete photos
  • Set primary photo

Bulk Actions:

  • Upload multiple photos
  • Clear all photos

5. Workspace Overview (/admin/workspaces/[id]/overview)

Dashboard view of workspace health.

Sections:

Status Card

  • Workspace status (active/paused/archived)
  • Quick actions: Pause/Resume, Archive

Stats Cards (grid)

  • Total listings
  • Active listings
  • Pending listings
  • Closed listings (last 30 days)

Domains

  • List of configured domains
  • Primary indicator
  • Add domain button

Last Sync

  • Timestamp
  • Status (success/error)
  • Stats (fetched, upserted, indexed)
  • Link to sync monitor
  • "Configure MLS" → /mls
  • "Edit Mapping" → /mapping
  • "View Sync Logs" → /sync
  • "Run Sync Now" → trigger sync

5. MLS Connection (/admin/workspaces/[id]/mls)

Configure Bridge API connection.

Form Fields:

FieldTypeNotes
Providerselectbridge (readonly for now)
Base URLtextDefault: https://api.bridgedataoutput.com/api/v2/OData
Dataset IDtexte.g., test or production dataset
Access TokenpasswordEncrypted on save
Sync Cadenceselectmanual / hourly / daily

Actions:

  • "Test Connection" button
    • Calls testConnection(workspaceId)
    • Shows success/error toast
    • On success, shows sample listing count
  • "Save" button
    • Upserts mls_connections row
    • Encrypts token before storing

Test Connection UI:

┌─────────────────────────────────────┐
│ ✓ Connection successful!            │
│   Found 2,847 listings in dataset   │
└─────────────────────────────────────┘

6. Field Mapping (/admin/workspaces/[id]/mapping)

Edit RESO → canonical field mappings.

UI Layout:

  • Left: JSON editor (Monaco or CodeMirror)
  • Right: Field reference + validation status

Features:

  • Load current active mapping
  • Edit JSON
  • "Validate" button
    • Calls validateMapping(workspaceId)
    • Fetches 5 sample listings
    • Shows which fields mapped successfully
    • Shows missing/unmapped fields
  • "Save as New Version" button
    • Creates new field_mappings row with version++
    • Sets is_active = true, deactivates previous

Validation Display:

✓ source_listing_key: ListingKey → "12345ABC"
✓ standard_status: StandardStatus → "Active"
✓ city: City → "Miami Beach"
✗ building_name: BuildingName → null (field not in source)
⚠ lot_size_sqft: LotSizeSquareFeet → undefined (check field name)

Version History:

  • Table of previous mapping versions
  • Created date
  • "Restore" action (creates new version from old)

7. Search Config (/admin/workspaces/[id]/search)

Configure Typesense search behavior.

Facet Toggles: Checkboxes for each available facet:

  • City
  • Subdivision
  • Building Name
  • Property Type
  • Status
  • Bedrooms
  • Bathrooms
  • Price Range

Sort Options: Drag-and-drop reorder:

  1. Newest First
  2. Price: Low to High
  3. Price: High to Low
  4. Bedrooms
  5. Square Footage

Default Sort: Dropdown to select default

Save: Updates workspace config (could be JSON in workspaces table or separate table)

8. Compliance (/admin/workspaces/[id]/compliance)

MLS attribution and disclaimer configuration.

Fields:

FieldTypeNotes
Attribution HTMLtextarea/rich textRequired MLS text
Disclaimer HTMLtextarea/rich textLegal disclaimer
Show on SearchcheckboxDisplay on search results
Show on DetailcheckboxDisplay on listing detail

Preview: Live preview of rendered HTML

Save: Upserts compliance_profiles row

Default Attribution Example:

<p>Listing information provided by Miami Realtors®.
Information deemed reliable but not guaranteed.</p>

9. Sync Monitor (/admin/workspaces/[id]/sync)

View sync history and trigger manual syncs.

Actions:

  • "Run Full Sync" button (with confirmation)
  • "Run Incremental Sync" button

Sync Runs Table:

ColumnDescription
StartedTimestamp
DurationTime to complete
Statusrunning/success/error (badge)
FetchedListings fetched from API
UpsertedListings written to DB
IndexedDocuments indexed to Typesense
ErrorsError count
ActionsView details

Filters:

  • Status filter (all/success/error)
  • Date range

Sync Detail Modal:

  • Full stats JSON
  • Error message (if failed)
  • Timestamp details

Live Status (when sync running):

┌─────────────────────────────────────┐
│ 🔄 Sync in progress...              │
│    Fetched: 847 / ~2,800            │
│    Elapsed: 2m 34s                  │
└─────────────────────────────────────┘

Components

Shared Components

/components/admin
├── AdminLayout.tsx          # Sidebar + header wrapper
├── WorkspaceSidebar.tsx     # Workspace-specific nav
├── StatusBadge.tsx          # Status indicator (active/paused/error)
├── StatCard.tsx             # Metric display card
├── DataTable.tsx            # Reusable table with sorting/filtering
├── JsonEditor.tsx           # Monaco-based JSON editor
├── ConfirmDialog.tsx        # Confirmation modal
└── Toast.tsx                # Toast notifications

Admin Layout

┌─────────────────────────────────────────────────────────┐
│ 1 OAK Admin                              [User] [Logout] │
├──────────────┬──────────────────────────────────────────┤
│              │                                          │
│  Workspaces  │                                          │
│  ─────────── │           [Content Area]                 │
│  > Lainey    │                                          │
│    Client 2  │                                          │
│              │                                          │
│  ─────────── │                                          │
│  + New       │                                          │
│              │                                          │
└──────────────┴──────────────────────────────────────────┘

Workspace Sidebar (when viewing workspace)

┌──────────────┐
│ ← Back       │
│              │
│ Lainey Levin │
│ ═══════════  │
│              │
│ Overview     │
│ Settings     │
│ Service Areas│
│ Imports      │
│ Photos       │
│ ───────────  │
│ MLS          │
│ Mapping      │
│ Search       │
│ Compliance   │
│ Sync         │
│              │
└──────────────┘

API Routes

// Workspace CRUD
POST   /api/admin/workspaces
GET    /api/admin/workspaces
GET    /api/admin/workspaces/[id]
PATCH  /api/admin/workspaces/[id]
DELETE /api/admin/workspaces/[id]

// MLS Connection
GET    /api/admin/workspaces/[id]/mls
PUT    /api/admin/workspaces/[id]/mls
POST   /api/admin/workspaces/[id]/mls/test

// Import Tasks
GET    /api/admin/workspaces/[id]/imports
POST   /api/admin/workspaces/[id]/imports
PATCH  /api/admin/workspaces/[id]/imports/[taskId]
DELETE /api/admin/workspaces/[id]/imports/[taskId]
POST   /api/admin/workspaces/[id]/imports/[taskId]/toggle
POST   /api/admin/workspaces/[id]/imports/[taskId]/run

// Field Mapping
GET    /api/admin/workspaces/[id]/mapping
POST   /api/admin/workspaces/[id]/mapping
POST   /api/admin/workspaces/[id]/mapping/validate

// Compliance
GET    /api/admin/workspaces/[id]/compliance
PUT    /api/admin/workspaces/[id]/compliance

// Sync
GET    /api/admin/workspaces/[id]/sync/runs
POST   /api/admin/workspaces/[id]/sync/run
GET    /api/admin/workspaces/[id]/sync/runs/[runId]

Dashboard API (Agent-facing)

// Service Areas
GET    /api/dashboard/service-areas
POST   /api/dashboard/service-areas
PUT    /api/dashboard/service-areas/[id]
DELETE /api/dashboard/service-areas/[id]
GET    /api/dashboard/service-areas/import-status

// Imports
GET    /api/dashboard/imports
POST   /api/dashboard/imports/[id]/sync
POST   /api/dashboard/imports/from-service-areas

// Photos
GET    /api/dashboard/photos?listing_id=xxx
POST   /api/dashboard/photos
DELETE /api/dashboard/photos/[id]
PATCH  /api/dashboard/photos/[id]
POST   /api/dashboard/photos/reorder

Authentication & Authorization

Middleware

// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';

export async function middleware(request: NextRequest) {
  const res = NextResponse.next();
  const supabase = createMiddlewareClient({ req: request, res });

  const { data: { session } } = await supabase.auth.getSession();

  // Protect /admin routes
  if (request.nextUrl.pathname.startsWith('/admin')) {
    if (!session) {
      return NextResponse.redirect(new URL('/admin/login', request.url));
    }

    // Check admin role
    const { data: profile } = await supabase
      .from('user_profiles')
      .select('role')
      .eq('id', session.user.id)
      .single();

    if (profile?.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }
  }

  return res;
}

On this page