/
Documentation

Tutorial: Customer Insights Dashboard (Next.js)

Build a Next.js add-on that shows revenue analytics, top customers, and overdue invoice alerts — a real-time business intelligence widget.

What you'll learn

• Setting up a Next.js 14+ project as a SyncBooks add-on
• Using App Router with the SDK provider in a client layout
• Fetching multiple data sources (invoices, customers, payments)
• Building charts and KPI cards
• Handling loading states with Suspense
• Deploying to Vercel and registering on the marketplace

Final result:

A dashboard widget that displays monthly revenue, top 5 customers by spend, overdue invoice count with total amount, and a 6-month revenue trend chart.

Difficulty: IntermediateTime: 30 minutesFramework: Next.js 14+Scopes: invoices.read, customers.read, payments.read, reports.read

Step 1: Create the Next.js Project

Terminal
# Create Next.js app with TypeScript and Tailwind
npx create-next-app@latest customer-insights --typescript --tailwind --app --src-dir
cd customer-insights

# Install the SyncBooks SDK
npm install @syncbooks/addon-sdk

Why Next.js for add-ons?

Next.js gives you App Router for clean layouts, built-in TypeScript, Tailwind CSS, and easy Vercel deployment. Add-ons run in an iframe, so you get full Next.js features — just no SSR for the SDK (it needs the browser window).

Step 2: Create the Provider Layout

Since the SyncBooks SDK needs browser APIs, we wrap it in a client component layout:

src/app/layout.tsx
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'Customer Insights — SyncBooks Add-on',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="bg-white text-gray-900 antialiased">
        {children}
      </body>
    </html>
  )
}
src/app/providers.tsx
"use client"

import { SyncBooksProvider } from '@syncbooks/addon-sdk'

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SyncBooksProvider>
      {children}
    </SyncBooksProvider>
  )
}
src/app/page.tsx
import Providers from './providers'
import Dashboard from './components/dashboard'

export default function Home() {
  return (
    <Providers>
      <Dashboard />
    </Providers>
  )
}

Important: 'use client' for SDK

The SyncBooksProvider must be in a client component because it communicates with the parent window via postMessage. Put it in a separate providers.tsx file and import it into your page.

Step 3: Build the Dashboard Component

src/app/components/dashboard.tsx
"use client"

import { useSyncBooks, useInvoices, useCustomers } from '@syncbooks/addon-sdk'
import { useState, useEffect, useMemo } from 'react'

export default function Dashboard() {
  const { ready, context, api } = useSyncBooks()
  const { data: invoices, loading: invLoading } = useInvoices({ limit: 100 })
  const { data: customers, loading: custLoading } = useCustomers({ limit: 50 })
  const [reportData, setReportData] = useState<any>(null)

  // Fetch financial summary via direct API call
  useEffect(() => {
    if (!ready || !api) return
    api.get('/reports/summary').then(res => {
      if (res.data) setReportData(res.data)
    })
  }, [ready, api])

  if (!ready || invLoading || custLoading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-center">
          <div className="w-8 h-8 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin mx-auto" />
          <p className="mt-3 text-sm text-gray-500">Loading insights...</p>
        </div>
      </div>
    )
  }

  // ─── Compute Metrics ──────────────────────────────────────────
  const overdueInvoices = invoices?.filter(inv => inv.status === 'overdue') || []
  const overdueTotal = overdueInvoices.reduce((sum, inv) => sum + (inv.totalAmount || 0), 0)
  const paidInvoices = invoices?.filter(inv => inv.status === 'paid') || []
  const totalRevenue = reportData?.totalRevenue || paidInvoices.reduce((sum, inv) => sum + (inv.totalAmount || 0), 0)

  // Top 5 customers by invoice amount
  const customerSpend: Record<string, { name: string; total: number }> = {}
  invoices?.forEach(inv => {
    const custId = inv.customerId?._id || inv.customerId
    const custName = inv.customerId?.name || 'Unknown'
    if (!customerSpend[custId]) customerSpend[custId] = { name: custName, total: 0 }
    customerSpend[custId].total += inv.totalAmount || 0
  })
  const topCustomers = Object.values(customerSpend)
    .sort((a, b) => b.total - a.total)
    .slice(0, 5)

  return (
    <div className="p-4 space-y-6 max-w-4xl mx-auto">
      {/* Header */}
      <div>
        <h1 className="text-xl font-bold text-gray-900">Customer Insights</h1>
        <p className="text-sm text-gray-500">Real-time business intelligence for {context?.organizationName}</p>
      </div>

      {/* KPI Cards */}
      <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
        <KPICard label="Total Revenue" value={formatCurrency(totalRevenue)} color="emerald" />
        <KPICard label="Invoices Sent" value={String(invoices?.length || 0)} color="blue" />
        <KPICard label="Overdue" value={String(overdueInvoices.length)} subtext={formatCurrency(overdueTotal)} color="red" />
        <KPICard label="Customers" value={String(customers?.length || 0)} color="purple" />
      </div>

      {/* Top Customers */}
      <div className="bg-white border rounded-lg p-4">
        <h2 className="text-sm font-semibold text-gray-700 mb-3">Top Customers by Revenue</h2>
        {topCustomers.length === 0 ? (
          <p className="text-sm text-gray-400">No invoice data yet</p>
        ) : (
          <div className="space-y-2">
            {topCustomers.map((cust, i) => (
              <div key={i} className="flex items-center justify-between py-2 border-b last:border-0">
                <div className="flex items-center gap-2">
                  <span className="w-6 h-6 rounded-full bg-emerald-100 text-emerald-700 text-xs font-bold flex items-center justify-center">
                    {i + 1}
                  </span>
                  <span className="text-sm font-medium">{cust.name}</span>
                </div>
                <span className="text-sm font-bold text-gray-900">{formatCurrency(cust.total)}</span>
              </div>
            ))}
          </div>
        )}
      </div>

      {/* Overdue Alert */}
      {overdueInvoices.length > 0 && (
        <div className="bg-red-50 border border-red-200 rounded-lg p-4">
          <h3 className="text-sm font-semibold text-red-800">⚠️ Overdue Invoices ({overdueInvoices.length})</h3>
          <p className="text-xs text-red-600 mt-1">Total outstanding: {formatCurrency(overdueTotal)}</p>
          <div className="mt-2 space-y-1">
            {overdueInvoices.slice(0, 5).map(inv => (
              <div key={inv._id} className="flex justify-between text-xs">
                <span className="text-red-700">{inv.invoiceNumber} — {inv.customerId?.name || 'N/A'}</span>
                <span className="font-medium text-red-800">{formatCurrency(inv.totalAmount)}</span>
              </div>
            ))}
            {overdueInvoices.length > 5 && (
              <p className="text-xs text-red-500 mt-1">+ {overdueInvoices.length - 5} more</p>
            )}
          </div>
        </div>
      )}
    </div>
  )
}

// ─── Helper Components ──────────────────────────────────────────

function KPICard({ label, value, subtext, color }: { label: string; value: string; subtext?: string; color: string }) {
  const colors: Record<string, string> = {
    emerald: 'bg-emerald-50 border-emerald-200 text-emerald-700',
    blue: 'bg-blue-50 border-blue-200 text-blue-700',
    red: 'bg-red-50 border-red-200 text-red-700',
    purple: 'bg-purple-50 border-purple-200 text-purple-700',
  }
  return (
    <div className={`rounded-lg border p-3 ${colors[color] || colors.blue}`}>
      <p className="text-xs opacity-80">{label}</p>
      <p className="text-lg font-bold mt-0.5">{value}</p>
      {subtext && <p className="text-xs opacity-70 mt-0.5">{subtext}</p>}
    </div>
  )
}

function formatCurrency(amount: number): string {
  return new Intl.NumberFormat('en-GH', { style: 'currency', currency: 'GHS' }).format(amount)
}

Step 4: Add the Manifest

Create a syncbooks-addon.json at the project root — this tells SyncBooks about your add-on:

syncbooks-addon.json
{
  "name": "Customer Insights Dashboard",
  "shortDescription": "Real-time revenue analytics, top customers, and overdue alerts",
  "version": "1.0.0",
  "launchUrl": "https://your-addon.vercel.app",
  "permissions": ["invoices.read", "customers.read", "payments.read", "reports.read"],
  "placements": ["dashboard_widget"],
  "category": "analytics",
  "events": ["invoice.created", "invoice.paid", "payment.received"]
}

Step 5: Deploy to Vercel

Terminal
# Push to GitHub
git init && git add . && git commit -m "initial"
gh repo create customer-insights --public --push

# Deploy (Vercel auto-detects Next.js)
npx vercel --prod

After deployment, copy your Vercel URL (e.g., https://customer-insights-xyz.vercel.app).

Step 6: Register on the Marketplace

1

Go to /developers

Go to /developers in SyncBooks
2

Open Marketplace Add-ons

Click "Marketplace Add-ons" in the sidebar
3

Submit Add-on

Click "Submit Add-on"
4

Fill in the details

Fill in:
  • Name: Customer Insights Dashboard
  • Short Description: Real-time revenue analytics, top customers, and overdue alerts
  • Launch URL: https://your-addon.vercel.app
  • Category: Analytics
  • Permissions: invoices.read, customers.read, payments.read, reports.read
  • Placement: Dashboard Widget
5

Submit for Review

Click "Submit for Review" — once approved, it appears in the marketplace!

What You Built

✅ Complete Next.js add-on with:

  • App Router + client component SDK integration
  • Real-time data fetching via SDK hooks
  • Direct API calls for summary reports
  • KPI cards, ranked customer list, overdue alerts
  • Responsive design that works in the sidebar or widget slot
  • One-command Vercel deployment

Next Steps

  • Add a date range picker to filter by month/quarter
  • Subscribe to invoice.paid webhook to update in real-time
  • Add a "Send Reminder" button for overdue invoices using notifications API
  • Store user preferences (default view, currency format) using the config API
  • Add charts using recharts or chart.js for revenue trends