# MapOps Expansion Workspace — Technical Documentation

This document describes how the **highlands-growth** application is built, how data flows through it, and how to extend or operate it. The product is marketed as **MapOps Expansion Workspace** (HVAC industry model for Australia).

---

## Purpose

The system answers one primary question for field-service operators:

> **Where should this business deploy its next crew — and which accounts should reps work first?**

It combines **regional open data**, **client customer history**, **territory scoring**, **interactive maps**, and **field sales workflow** into a single workspace per client/region.

---

## Stack

| Layer | Technology |
| --- | --- |
| Frontend | Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS v4 |
| Maps | MapLibre GL |
| Validation | Zod (`src/lib/workspace/schema.ts`) |
| Persistence | Supabase (Postgres + PostGIS), localStorage fallbacks |
| Auth | Supabase Auth (magic link) |
| Data ingest | Node.js scripts (`ingest/`), no API keys for public sources |
| CRM | Provider-agnostic connector layer (`src/lib/crm/`) |

---

## Repository layout

```
highlands-growth/
├── data/regions/           # Ingested regional intelligence (git-ignored at scale)
│   ├── registry.json       # Region definitions, demo workspaces, depots
│   └── <region-id>/        # boundaries, census, erp, climate, osm, approvals…
├── docs/                   # This file + user guide
├── ingest/                 # Regional data pipeline
├── src/
│   ├── app/                # Routes (dashboard, preview, proof, API)
│   ├── components/         # UI (dashboard, outreach, customers, marketing)
│   └── lib/                # Business logic
│       ├── workspace/      # Bundle assembly, scoring inputs, map layers
│       ├── regions/        # Registry, workspaces, paths
│       ├── scoring/        # Suburb scoring engine
│       ├── outreach/       # Rep workflow, assign rules, exports
│       ├── customers/      # CSV import, geocode, CRM rollups
│       ├── crm/            # Multi-CRM connectors + webhooks
│       └── supabase/       # DB access helpers
└── supabase/migrations/    # Postgres schema
```

---

## Core concepts

### Workspace

A **workspace** is a configured client environment, e.g. `demo-workspace` (alias for `demo-illawarra`). Each workspace maps to:

- A **region** (`regionId` → bbox, depots, climate stations)
- A **client profile** (company, service type, depot, expansion question)
- A **WorkspaceBundle** — the JSON contract consumed by the dashboard

Workspace config lives in `data/regions/registry.json` (`demos[]`) and is resolved via `src/lib/regions/workspaces.ts`.

### Region

A **region** is a geographic package: SA2 boundaries, census demographics, ERP population, climate, building approvals, OSM businesses, etc. Stored under `data/regions/<region-id>/`.

Macro-regions for NSW batch ingest are defined in `ingest/lib/nsw-pack.mjs`. Statewide foundation is `nsw-state`; macro-regions sync from it via `ingest/sync-foundation.mjs`.

### WorkspaceBundle

The bundle (`src/lib/workspace/schema.ts`) is the single source of truth for the dashboard:

- `manifest` — client, tabs, layer definitions
- `decision` — executive summary scores and conclusion
- `suburbs` — scored SA2-level areas
- `markets` — grouped market cards
- `targets` — referral partners (where seeded)
- `map` — viewport, layer GeoJSON payloads

Built server-side in `loadWorkspaceBundle.ts`:

1. Shell from workspace config (`buildWorkspaceShell.ts`)
2. Region areas + census + commercial enrichment
3. Scoring (`scoreSuburbs`)
4. OSM layers (`regionLayers.ts`)
5. Drive-time matrix from depot (`driveTimeMatrix.ts`)
6. CRM / penetration / priority choropleths

### Geography (SA2)

Scoring and map choropleths use **ABS SA2** (2021 ASGS) as the primary unit. Suburb names come from boundary properties. Point-in-polygon lookup for customer geocoding uses `src/lib/geo/sa2Lookup.ts`.

---

## Request flow (dashboard)

```
GET /dashboard/[workspaceId]
  → loadWorkspaceBundle(workspaceId)     # server component / loader
  → WorkspaceDashboardShell              # providers: scoring, outreach, depot, customers
      → MapStage                         # MapLibre + layer toggles
      → InsightsRail                     # tabbed side panel
```

API:

- `GET /api/workspace/[workspaceId]` — full bundle JSON
- `GET /api/workspace/[workspaceId]/preview` — lightweight preview payload

---

## Scoring engine

Location: `src/lib/scoring/score.ts`

Each suburb receives **six component scores** (demand, evidence, competition, reachability, operational fit, plus derived opportunity), weighted by industry profile (`src/lib/industry/hvac.ts`).

Inputs include:

- Census 2021 (income, dwellings, tenure, age of stock)
- **ERP 2025** population and YoY growth (`erpPopulation`, `erpGrowthPct`)
- Climate (HDD/CDD, thermal load)
- Building approvals (trailing 12 months)
- OSM commercial density and segment mix
- Client CRM rollups (when customers uploaded)
- Drive time from selected **depot**

### Scoring modes

| Mode | ID | Use case |
| --- | --- | --- |
| Greenfield | `greenfield` | New territory — B2B density, white space |
| Penetration | `penetration` | Deepen share where you already have customers |

Mode is persisted per workspace in localStorage and reflected in URL `?mode=penetration`. Priority map layers refresh via `refreshPriorityLayerData`.

---

## Map layers

`src/lib/workspace/regionLayers.ts` loads ingested GeoJSON/JSON from disk at request time (server-only).

Layer groups include:

- **Regional intelligence (OSM)** — named businesses, trade competitors
- **Demographics (Census + ERP)** — income, dwellings, ERP population/growth choropleths
- **Climate (BoM)** — heating/cooling degree days
- **CRM** — revenue/conversion rollups when customer data present
- **Penetration** — customer density vs market size

`next.config.ts` `outputFileTracingIncludes` ensures regional data files are bundled into serverless functions for workspace routes.

---

## Regional data ingest

See `ingest/README.md` for commands.

Pipeline:

1. **Foundation** (`nsw-state`) — boundaries, ERP, approvals, census, climate
2. **Sync** — clip foundation into macro-regions by bbox
3. **OSM** — Overpass API, tiled for large regions (`ingest/osm.mjs`)

Outputs per region:

| Path | Source |
| --- | --- |
| `boundaries/sa2.geojson` | ABS ASGS |
| `census/sa2_demographics.geojson` | ABS Census API + ERP merge |
| `erp/sa2_erp.json` | ABS annual ERP |
| `climate/sa2_climate.geojson` | BoM stations + IDW |
| `approvals/sa2_approvals.json` | ABS building approvals |
| `osm/businesses.geojson` | OpenStreetMap / Overpass |

Cache: `ingest/.cache/`. Re-runs are incremental.

---

## Customer data

Flow:

1. User uploads CSV → column mapping UI (`CustomerUploadPanel`)
2. `POST /api/customers/geocode` — Nominatim geocode, SA2 lookup
3. Persist to Supabase `customers` (or localStorage if unconfigured)
4. `CustomerContext` merges into suburbs → rescoring + penetration layer

CRM rollups per SA2: jobs, revenue, margin, conversion (`src/lib/customers/crmRollup.ts`).

---

## Field sales (outreach)

State: `src/lib/outreach/store.ts` — status per OSM business ID (`new` → `won` / `pass`).

Persistence:

- **localStorage** — immediate UX
- **Supabase** — `outreach_leads`, `workspace_reps`, `outreach_settings` via debounced `PUT /api/outreach/[workspaceId]`

Features:

- Territory selection → ranked **TargetRow** list (`territoryPlan.ts`)
- Lasso assign, assignment board, smart assign rules
- Rep mode — simplified UI (Field map + My calls)
- Leader scorecard, today's queue
- Prospect proof URLs → `/proof/[workspaceId]?area=…`

---

## CRM integration

Location: `src/lib/crm/`

| Direction | Action |
| --- | --- |
| Pull customers | CRM deals/contacts → geocode → `customers` table |
| Push outreach | Ranked leads + rep status → CRM deals |
| Webhooks | CRM stage change → update `outreach_leads` |
| Poll | `pull_outreach_status` — manual sync fallback |

Providers: **Pipedrive** (API token), **HubSpot** (OAuth), scaffolds for Zoho/Salesforce.

Database: `crm_connections`, `crm_entity_links`, `crm_sync_runs`, `crm_webhook_events` (migrations `0004`, `0005`).

Webhook URL: `/api/crm/webhooks/[token]` — unique token per workspace connection.

---

## Database (Supabase)

Key tables:

| Table | Purpose |
| --- | --- |
| `projects` | Workspace slug → project id |
| `customers` | Geocoded job history |
| `outreach_leads` | Per-business outreach state |
| `crm_connections` | Encrypted CRM credentials |
| `crm_entity_links` | MapOps id ↔ CRM external id |

Migrations: `supabase/migrations/0001_init.sql` through `0005_crm_webhooks.sql`.

RLS enabled on `customers` and `projects`; outreach tables use service-role/admin paths for webhooks.

---

## Auth & routes

| Route | Access |
| --- | --- |
| `/dashboard/[workspaceId]` | Auth + workspace membership (demo workspaces public) |
| `/preview/[workspaceId]` | Static marketing previews |
| `/proof/[workspaceId]` | Prospect-facing local market story |
| `/coverage/[workspaceId]` | Coverage view |
| `/login` | Supabase magic link |

Public demo IDs: `demo-workspace`, `demo-illawarra`, `demo-newcastle` (`publicDemoAccess.ts`).

---

## Deployment

- **Hosting**: Vercel (typical)
- **Env vars**: `NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, `CRM_TOKEN_SECRET`, `HUBSPOT_CLIENT_*` (optional)
- **Data size**: Deploy only regions needed per client; full NSW OSM ~750MB on disk — use `.vercelignore` patterns in repo
- **Build**: `npm run build` — validates types + bundles traced region files

---

## Extension points

| Goal | Where to start |
| --- | --- |
| New region | Add to `registry.json`, run `npm run ingest -- --region=…` |
| New industry | `src/lib/industry/`, scoring weights, `COMMERCIAL_PRIORITY` |
| New map layer | `regionLayers.ts` + ingest module |
| New CRM | Implement `CrmConnector` in `src/lib/crm/connectors/` |
| New score input | `score.ts` accessors + census/ingest field |
| Client workspace | New `demos[]` entry + optional Supabase project row |

---

## Related docs

- [User guide](./USER-GUIDE.md) — how to use the dashboard
- [Ingest README](../ingest/README.md) — regional data commands
- [CRM README](../src/lib/crm/README.md) — connector API and webhooks
