Menu

Change language

GitHub repo

Leaflet Sales Territory Map

blockanalytics

Sales territory map on free OpenStreetMap/Esri tiles — no API key. Region boundary polygons and rep pins with quota attainment; select a territory to fly the camera.

Installation

$npx uipkge-ng@latest add leaflet-sales-territory-map
Manifest:https://uipkge.dev/r/angular/leaflet-sales-territory-map.jsonInstalls to:src/app/components/blocks/leaflet-sales-territory-map/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

SalesTerritory
interface SalesTerritory {
  id: string
  name: string
  code: string
  rep: string
  arr: string
  attainment: number
  status: TerritoryStatus
  /** Territory centroid as [lng, lat]. */
  centerLngLat: [number, number]
  /** Territory boundary ring as [lng, lat][]. */
  polygon: [number, number][]
}

Files installed (2)

  • src/app/components/blocks/leaflet-sales-territory-map/leaflet-sales-territory-map.component.ts8.6 kB
    import { Component, Input, OnInit, ViewChild, signal, ChangeDetectionStrategy } from '@angular/core'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiLeafletMapComponent,
      UiLeafletMarkerComponent,
      UiLeafletPolygonComponent,
    } from '@/ui/leaflet-map/leaflet-map.component'
    
    export type TerritoryStatus = 'attained' | 'ontrack' | 'attention'
    
    export interface SalesTerritory {
      id: string
      name: string
      code: string
      rep: string
      arr: string
      attainment: number
      status: TerritoryStatus
      /** Territory centroid as [lng, lat]. */
      centerLngLat: [number, number]
      /** Territory boundary ring as [lng, lat][]. */
      polygon: [number, number][]
    }
    
    // Hand-drawn plausible US region boundaries — four rep territories tiling CONUS.
    export const DEFAULT_TERRITORIES: SalesTerritory[] = [
      {
        id: 'pacific-west',
        name: 'Pacific West',
        code: 'PAC',
        rep: 'Sierra Delgado',
        arr: '$2.8M',
        attainment: 112,
        status: 'attained',
        centerLngLat: [-119.8, 39.5],
        polygon: [
          [-124.4, 32.5],
          [-114.0, 32.5],
          [-114.0, 35.0],
          [-114.6, 39.0],
          [-120.0, 42.0],
          [-117.0, 45.9],
          [-117.0, 49.0],
          [-124.7, 48.4],
          [-124.2, 40.0],
          [-124.4, 32.5],
        ],
      },
      {
        id: 'mountain-central',
        name: 'Mountain Central',
        code: 'MTN',
        rep: 'Colt Brannigan',
        arr: '$2.2M',
        attainment: 97,
        status: 'ontrack',
        centerLngLat: [-104.8, 39.9],
        polygon: [
          [-117.0, 49.0],
          [-95.0, 49.0],
          [-95.0, 43.5],
          [-97.5, 40.0],
          [-95.5, 36.5],
          [-100.0, 34.5],
          [-103.0, 32.0],
          [-106.5, 31.8],
          [-108.2, 31.3],
          [-111.0, 31.3],
          [-114.0, 32.5],
          [-114.0, 35.0],
          [-114.6, 39.0],
          [-120.0, 42.0],
          [-117.0, 45.9],
          [-117.0, 49.0],
        ],
      },
      {
        id: 'southeast',
        name: 'Southeast',
        code: 'SE',
        rep: 'Harper Quinn',
        arr: '$1.9M',
        attainment: 84,
        status: 'attention',
        centerLngLat: [-83.0, 33.5],
        polygon: [
          [-95.5, 36.5],
          [-94.0, 33.0],
          [-93.0, 30.0],
          [-89.0, 29.0],
          [-84.0, 29.9],
          [-82.6, 28.0],
          [-81.0, 25.8],
          [-80.1, 25.1],
          [-80.5, 26.8],
          [-81.4, 30.4],
          [-80.8, 32.2],
          [-77.5, 34.0],
          [-75.5, 35.5],
          [-76.0, 36.9],
          [-77.5, 39.0],
          [-80.5, 40.8],
          [-83.0, 39.5],
          [-84.8, 38.0],
          [-88.0, 37.0],
          [-90.5, 36.0],
          [-95.5, 36.5],
        ],
      },
      {
        id: 'northeast',
        name: 'Northeast',
        code: 'NE',
        rep: 'Devon Whitaker',
        arr: '$3.4M',
        attainment: 121,
        status: 'attained',
        centerLngLat: [-74.5, 41.8],
        polygon: [
          [-95.0, 49.0],
          [-95.0, 45.9],
          [-92.5, 44.5],
          [-89.5, 43.0],
          [-87.0, 41.5],
          [-83.5, 41.7],
          [-79.8, 42.3],
          [-76.8, 43.5],
          [-74.0, 45.0],
          [-71.0, 45.0],
          [-70.0, 43.6],
          [-70.9, 42.9],
          [-74.0, 40.5],
          [-75.0, 38.5],
          [-77.5, 39.0],
          [-80.5, 40.8],
          [-83.0, 39.5],
          [-84.8, 38.0],
          [-88.0, 37.0],
          [-90.5, 36.0],
          [-95.5, 36.5],
          [-97.5, 40.0],
          [-95.0, 43.5],
          [-95.0, 49.0],
        ],
      },
    ]
    
    /** Plain-hex fills — Leaflet renders paths to SVG/canvas, so CSS vars do not resolve there. */
    export const TERRITORY_STATUS_STYLES: Record<TerritoryStatus, { fill: string; stroke: string }> = {
      attained: { fill: '#22c55e', stroke: '#4ade80' },
      ontrack: { fill: '#38bdf8', stroke: '#7dd3fc' },
      attention: { fill: '#f59e0b', stroke: '#fbbf24' },
    }
    
    // Geographic center of CONUS — stable reference so the map's center input never re-fires.
    const US_CENTER: [number, number] = [-98.5795, 39.8283]
    
    const PIN_SELECTED =
      'border-primary bg-primary text-primary-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'
    const PIN_IDLE =
      'border-border bg-card text-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'
    
    /**
     * Sales territory map on free OpenStreetMap/Esri tiles — no API key. Region
     * boundary polygons and rep pins with quota attainment; selecting a territory
     * (polygon, pin or quota row) flies the camera to it.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-leaflet-sales-territory-map, [ui-leaflet-sales-territory-map]',
      standalone: true,
      imports: [
        UiBadgeComponent,
        UiCardComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCardContentComponent,
        UiLeafletMapComponent,
        UiLeafletMarkerComponent,
        UiLeafletPolygonComponent,
      ],
      host: {
        'data-slot': 'leaflet-sales-territory-map',
        '[class]': 'hostClass',
      },
      template: `
        <div class="flex flex-wrap items-center justify-between gap-3">
          <h2 class="text-foreground text-lg font-semibold tracking-tight">Territories</h2>
          <p class="text-muted-foreground text-xs">{{ territories.length }} regions</p>
        </div>
    
        <div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12">
          <ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-8">
            <div class="relative min-h-[360px] flex-1 overflow-hidden">
              <ui-leaflet-map variant="dark" [center]="usCenter" [zoom]="4" class="absolute inset-0 size-full">
                @for (t of territories; track 'boundary-' + t.id) {
                  <ui-leaflet-polygon
                    [lngLatPath]="t.polygon"
                    [color]="strokeColor(t)"
                    [weight]="selectedId() === t.id ? 2.5 : 1.5"
                    [opacity]="0.9"
                    [fill]="true"
                    [fillColor]="fillColor(t)"
                    [fillOpacity]="selectedId() === t.id ? 0.3 : 0.14"
                    (layerClick)="selectTerritory(t)"
                  />
                }
                @for (t of territories; track t.id) {
                  <ui-leaflet-marker [lngLat]="t.centerLngLat" anchor="center">
                    <button type="button" [class]="pinClass(t)" (click)="selectTerritory(t)">
                      {{ t.code }} {{ t.attainment }}%
                    </button>
                  </ui-leaflet-marker>
                }
              </ui-leaflet-map>
            </div>
          </ui-card>
    
          <ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4">
            <ui-card-header class="border-border border-b p-4">
              <ui-card-title class="text-sm font-semibold">Quota</ui-card-title>
            </ui-card-header>
            <ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0">
              @for (t of territories; track t.id) {
                <button type="button" [class]="rowClass(t)" (click)="selectTerritory(t)">
                  <div class="min-w-0">
                    <p class="text-foreground truncate text-sm font-medium">{{ t.code }} · {{ t.name }}</p>
                    <p class="text-muted-foreground truncate font-mono text-xs tabular-nums">{{ t.rep }} · {{ t.arr }}</p>
                  </div>
                  <ui-badge [variant]="statusVariant(t.status)" class="shrink-0 text-xs tabular-nums"
                    >{{ t.attainment }}%</ui-badge
                  >
                </button>
              }
            </ui-card-content>
          </ui-card>
        </div>
      `,
    })
    export class UiLeafletSalesTerritoryMapComponent implements OnInit {
      @Input() territories: SalesTerritory[] = DEFAULT_TERRITORIES
      @Input('class') className?: string
    
      @ViewChild(UiLeafletMapComponent) map?: UiLeafletMapComponent
    
      protected readonly usCenter = US_CENTER
      readonly selectedId = signal('')
    
      get hostClass(): string {
        return cn('flex h-full min-h-0 flex-col gap-4', this.className)
      }
    
      ngOnInit(): void {
        this.selectedId.set(this.territories[0]?.id ?? '')
      }
    
      selectTerritory(territory: SalesTerritory): void {
        this.selectedId.set(territory.id)
        this.map?.flyTo({
          center: territory.centerLngLat,
          zoom: 5,
          duration: 800,
        })
      }
    
      statusVariant(status: TerritoryStatus): 'info' | 'secondary' | 'warning' {
        if (status === 'attained') return 'info'
        if (status === 'ontrack') return 'secondary'
        return 'warning'
      }
    
      strokeColor(t: SalesTerritory): string {
        return TERRITORY_STATUS_STYLES[t.status].stroke
      }
    
      fillColor(t: SalesTerritory): string {
        return TERRITORY_STATUS_STYLES[t.status].fill
      }
    
      pinClass(t: SalesTerritory): string {
        return this.selectedId() === t.id ? PIN_SELECTED : PIN_IDLE
      }
    
      rowClass(t: SalesTerritory): string {
        return cn(
          'hover:bg-accent/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors',
          this.selectedId() === t.id && 'bg-accent/60',
        )
      }
    }
    
  • src/app/components/blocks/leaflet-sales-territory-map/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/leaflet-sales-territory-map.json