Mikkel Manniche
Dansk English Deutsch

Template · React · HTML

Sales floor

A sales team dashboard in Tiles: a segmented target dial, an inverted live call card, performance bars per seller and a revenue donut.

Preview

React

Install the template with shadcn. It lands in your project as ordinary components that you edit yourself.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/sales-floor.json
npx shadcn@latest add @manniche/sales-floor
sales-floor.tsx
// A sales-floor screen: a segmented arc dial for the target, a live call card, performance bars and revenue by source.
import { Phone } from 'lucide-react'
import { cn } from '@/lib/utils'
import { BarChart, type BarPoint } from '@/registry/manniche/bar-chart/bar-chart'
import type { ValueFormat } from '@/registry/manniche/chart-kit/chart-utils'
import { DataTile } from '@/registry/manniche/data-tile/data-tile'
import { Dial, type DialZone } from '@/registry/manniche/dial/dial'
import { Donut, type DonutDatum } from '@/registry/manniche/donut/donut'

export type SalesFloorData = {
  target: { title: string; value: number; max: number; zones: DialZone[]; caption: string; format?: ValueFormat }
  call: { title: string; contact: string; company: string; status: string; duration: string; action: string }
  performance: { title: string; points: BarPoint[]; format?: ValueFormat; /** The bar to mark as current, if the bars are periods. Default none. */ current?: number | null }
  sources: { title: string; items: DonutDatum[]; format?: ValueFormat }
}

export type SalesFloorLabels = { note?: string }

export type SalesFloorProps = { data: SalesFloorData; labels?: SalesFloorLabels; onEndCall?: () => void; className?: string }

export function SalesFloor({ data, labels, onEndCall, className }: SalesFloorProps) {
  const note = labels?.note ?? 'Example data.'
  return (
    <div className={cn('@container w-full rounded-2xl bg-background p-3 text-foreground', className)}>
      <div className="grid grid-cols-1 gap-3 @2xl:grid-cols-6">
        <div className="@2xl:col-span-3">
          <DataTile className="h-full" title={data.call.title} inverted>
            <div className="flex flex-wrap items-center gap-3">
              <span aria-hidden className="grid size-11 place-items-center rounded-full bg-muted">
                <Phone className="size-5" />
              </span>
              <div className="min-w-0">
                <p className="truncate font-medium">{data.call.contact}</p>
                <p className="truncate text-sm opacity-70">{data.call.company}</p>
              </div>
              <span role="status" className="ml-auto text-sm whitespace-nowrap tabular-nums">
                {data.call.status} · {data.call.duration}
              </span>
            </div>
            <button
              type="button"
              onClick={onEndCall}
              className="mt-4 rounded-full bg-foreground px-4 py-2 text-sm font-medium text-background transition-transform active:scale-[0.97] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
            >
              {data.call.action}
            </button>
          </DataTile>
        </div>
        <div className="@2xl:col-span-3">
          <DataTile className="h-full" title={data.target.title} footer={note}>
            <Dial
              value={data.target.value}
              max={data.target.max}
              zones={data.target.zones}
              label={data.target.title}
              caption={data.target.caption}
              format={data.target.format}
              segments={24}
            />
          </DataTile>
        </div>
        <div className="@2xl:col-span-4">
          <DataTile className="h-full" title={data.performance.title} footer={note}>
            <BarChart data={data.performance.points} label={data.performance.title} format={data.performance.format} current={data.performance.current ?? null} />
          </DataTile>
        </div>
        <div className="@2xl:col-span-2">
          <DataTile className="h-full" title={data.sources.title} footer={note}>
            <Donut data={data.sources.items} label={data.sources.title} format={data.sources.format} />
          </DataTile>
        </div>
      </div>
    </div>
  )
}
sales-floor-demo.tsx
import { SalesFloor, type SalesFloorData } from '@/components/sales-floor'

// Example data only. All names and figures are invented.
const money = { currency: 'EUR', decimals: 0 }

const data: SalesFloorData = {
  call: { title: 'Live call', contact: 'Alex Example', company: 'Sample Co', status: 'Connected', duration: '04:12', action: 'End call' },
  target: {
    title: 'Monthly target',
    value: 62000,
    max: 100000,
    format: money,
    caption: 'of an example target',
    zones: [
      { label: 'Behind', to: 40000 },
      { label: 'On track', to: 80000 },
      { label: 'Ahead', to: 100000 },
    ],
  },
  performance: {
    title: 'Calls booked per rep',
    points: ['Ana', 'Ben', 'Cy', 'Di', 'Eli'].map((label, i) => ({ label, title: `${label} (example)`, value: [34, 28, 41, 22, 30][i] })),
  },
  sources: {
    title: 'Revenue by source',
    format: money,
    items: [
      { label: 'Outbound', value: 28000 },
      { label: 'Referral', value: 21000 },
      { label: 'Inbound', value: 13000 },
    ],
  },
}

export default function SalesFloorDemo() {
  return <SalesFloor data={data} />
}

HTML

The whole page as one file with all CSS built in and no JavaScript. Toggles, tabs and filters work with HTML and CSS alone.

Open in a new tab

The colour in your own app

If you already have shadcn set up, these variables give the same colour. Light in :root, dark in .dark.

:root {
  --primary: oklch(0.52 0.2 262);
  --primary-foreground: oklch(0.99 0 0);
  --accent: oklch(0.95 0.034 262);
  --accent-foreground: oklch(0.36 0.140 262);
  --ring: oklch(0.52 0.2 262);
}

.dark {
  --primary: oklch(0.74 0.150 262);
  --primary-foreground: oklch(0.18 0.024 262);
  --accent: oklch(0.29 0.050 262);
  --accent-foreground: oklch(0.9 0.050 262);
  --ring: oklch(0.74 0.150 262);
}

Install

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/sales-floor.json

Dependencies

Files

  • sales-floor.tsx
  • sales-floor.html

Manniche Lab · open source

Free tools for AI agents and web development.