# React CRM Template: Pipeline, Prognose, Firmenseiten

> Bau ein React CRM Template in Next.js aus Blöcken: ein Pipeline-Kanban mit gewichteter Prognose, ein Deal-Sheet, Firmenseiten und ein Layout fürs Smartphone.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. Okt. 2026
- Kategorie: [Praxis: Screens und Apps](https://wingo-ui.com/de/blog/category/build)
- Lesezeit: 11 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-crm-template
- Englische Fassung: https://wingo-ui.com/blog/react-crm-template

## Kurz gesagt

Ein React CRM Template braucht zuerst drei Screens: ein Board für die Deal-Pipeline mit gewichteter Prognose, ein Deal-Sheet zum Bearbeiten eines einzelnen Deals und eine Firmenseite mit Aktivitäten, Deals, Dateien und Notizen. In Next.js setzt du sie aus den Blöcken CRM Pipeline Page und CRM Contact Page von Wingo UI zusammen: Server-Seiten laden die Daten, eine kleine Client-View reicht Server Actions an den Block weiter, und jede Verschiebung wird optimistisch gespeichert, mit Rollback, falls das Speichern fehlschlägt. Auf dem Smartphone zeigt das Board jeweils eine Phase, und Sheets und Menüs öffnen sich von unten.

Du baust ein CRM-Frontend, für einen Kunden oder für dein eigenes Produkt, und suchst nach einem react crm template. Die Treffer sind Theme-Marktplätze mit Admin-Kits aus Dutzenden Seiten, Open-Source-CRMs mit eigenem Backend und ein paar Starter, die ein Kanban-Board mit Beispielzahlen zeigen. Was du brauchst, ist kleiner und schwieriger: eine Deal-Pipeline, deren Summen etwas bedeuten, eine Möglichkeit, einen Deal zu bearbeiten, ohne das Board zu verlassen, und eine Firmenseite, die die ganze Kundenbeziehung abbildet. Dieses Tutorial baut diese drei Screens in Next.js aus Blöcken von Wingo UI, verbunden mit Server Actions, und zeigt den fertigen Code.

## Was gehört in ein React CRM Template?

Der größte Teil des Nutzens eines CRM steckt in drei Screens. Alles andere (Aufgaben, E-Mail-Sync, Reports) kann später kommen.

1. **Das Pipeline-Board.** Deals als Karten in Spalten pro Phase, jede Spalte mit Anzahl, Summe und **gewichteter** Summe, darüber eine Zusammenfassung: offene Pipeline, gewichtete Prognose, diesen Monat gewonnen, Abschlussquote. Filter nach Verantwortlichen und Abschlussdatum. Eine Listenansicht für alle, die in Zeilen denken.
2. **Das Deal-Sheet.** Ein Klick auf eine Karte öffnet ein Panel mit Wert, Phase, Abschlussdatum, Verantwortlichem, Kontakt, Notizen und dem Aktivitätsverlauf. Wer es mit ungespeicherten Änderungen schließt, wird vorher gefragt.
3. **Der Firmendatensatz.** Eine Seite pro Kunde mit den wichtigsten Fakten, den Kontakten, den Deals, einer Aktivitäts-Timeline, Dateien und Notizen.

Die Details entscheiden, ob das Vertriebsteam dem Tool vertraut. Wer einen Deal auf „Verloren“ ablegt, sollte nach dem Grund gefragt werden, denn ein Report über Verlustgründe ist wertlos, wenn die Hälfte der Gründe fehlt. Ein Deal, der wochenlang in einer Phase liegt, sollte veraltet aussehen. Schlägt das Speichern fehl, sollte die Karte zurückspringen und das auch melden, statt ein Board zu hinterlassen, das lügt.

## Solltest du ein CRM-Template kaufen oder aus Blöcken zusammensetzen?

Setz es aus Blöcken zusammen, wenn das CRM ein Teil eines größeren Produkts ist, betreib Twenty, wenn du ein ganzes CRM mit Backend willst, und kauf ein Kit, wenn du am ersten Tag viele fertige Screens haben möchtest. Die Fakten stammen von den öffentlichen Seiten der Projekte, Stand Oktober 2026:

| Option | Was es ist | Stack | Pipeline-Berechnung |
|---|---|---|---|
| [Refine CRM example](https://refine.dev/templates/crm-application/) | Referenz-App: Dashboard, Kalender, Kanban, Sales-Pipeline, Firmen, Kontakte, Angebote; Repo unter MIT | React, Vite, Ant Design, GraphQL über Nestjs-query | Phasen und Conversions; Gewichtung nach Wahrscheinlichkeit nicht beschrieben |
| [TheFrontKit CRM Dashboard Kit](https://thefrontkit.com/crm-dashboard-template) | Kostenpflichtiges UI-Kit mit über 35 Screens, nur UI, $99 (reduziert von $165) | Next.js 16, Tailwind CSS v4, shadcn/ui, @hello-pangea/dnd | Spaltensummen; Prognose als committed, best case und pipeline |
| [Twenty](https://github.com/twentyhq/twenty) | Ein komplettes Open-Source-CRM, größtenteils AGPLv3 | React-Frontend, NestJS, PostgreSQL, Redis | Ein volles Produkt mit eigenem Backend |
| [Wingo UI CRM-Blöcke](https://wingo-ui.com/components/crm-pipeline-page) | Pro-Blöcke als Quellcode, die du in deine App einbaust | Next.js oder Vite, React 19, Tailwind CSS v4, dnd kit | Gewichtete Summen pro Phase, Prognose, Abschlussquote, veraltete Deals |

Twenty ist die bessere Wahl, wenn du ein CRM betreiben und erweitern willst und kein Frontend bauen möchtest. Ein komplettes Kit ist besser, wenn du Dutzende fertige Screens willst und die Beispieldaten ersetzen wirst. Blöcke passen, wenn das CRM in einem Agentur-Tool, einer Rechnungs-App oder einem vertikalen SaaS steckt und zum Rest passen muss. Ein Hinweis zu Refine: Laut README von `examples/app-crm` ist das volle Beispiel in die Enterprise Edition gewandert, und das MIT-Repo behält eine kleinere Community-Version in `examples/app-crm-minimal`.

## Wie strukturierst du ein Next.js CRM Template?

Ein nextjs crm template braucht zwei Routen in der Admin-Routengruppe aus unserem [Next.js Admin-Dashboard-Tutorial](https://wingo-ui.com/de/blog/nextjs-admin-dashboard-tutorial). Dort richten wir auch die Shell, den `LinkProvider` und die Datenzugriffsschicht mit `verifySession()` ein, die unten genutzt werden.

```text
app/(admin)/
  layout.tsx               AdminShell mit layout="fill"
  deals/
    page.tsx               lädt Pipelines, Deals, Verantwortliche, Firmen
    deals-view.tsx         Client: CrmPipelinePage + Actions
    actions.ts             moveDeal, saveDeal, createDeal, deleteDeal
  clients/[id]/
    page.tsx               lädt einen Firmendatensatz
    client-view.tsx        Client: CrmContactPage + Actions
    actions.ts             saveNote, deleteNote, setTags, addDeal
server/
  dal.ts                   verifySession
  crm.ts                   deine Queries, PIPELINES, toCrmDeal
```

Installier die beiden Blöcke und die Shell. Die CLI ergänzt alles, was sie importieren, auch die [Deal Pipeline](https://wingo-ui.com/components/deal-pipeline) und das [Kanban](https://wingo-ui.com/components/kanban) darunter:

```bash
npx wingo-ui@latest add crm-pipeline-page crm-contact-page admin-shell
```

## Wie baust du die Seite für die Deal-Pipeline?

Fang mit den Datenstrukturen an. Die Phasen stehen in der Konfiguration, denn eine geänderte Wahrscheinlichkeit ist eine Produktentscheidung und sollte wie jede andere Codeänderung durchs Review gehen. Die IDs der Phasen müssen über alle Pipelines eindeutig sein: Die Seite ordnet einen Deal der Pipeline zu, zu der seine Phase gehört.

```ts
// server/crm.ts
import "server-only";
import type { CrmDeal, CrmPipeline } from "@/components/blocks/crm-pipeline-page";

export const PIPELINES: CrmPipeline[] = [
  {
    id: "sales",
    name: "Vertrieb",
    stages: [
      { id: "qualified", name: "Qualifiziert", probability: 0.1 },
      { id: "discovery", name: "Bedarfsanalyse", probability: 0.25 },
      { id: "proposal", name: "Angebot gesendet", probability: 0.5 },
      { id: "negotiation", name: "Verhandlung", probability: 0.75 },
      { id: "won", name: "Gewonnen", probability: 1, kind: "won" },
      { id: "lost", name: "Verloren", probability: 0, kind: "lost" },
    ],
  },
];

export type DealRow = {
  id: string;
  stage_id: string;
  title: string;
  amount_cents: number;
  currency: string;
  probability: number | null;
  company_id: string;
  company_name: string;
  owner_id: string;
  owner_name: string;
  close_date: string | null;
  stage_entered_at: string;
  lost_reason: string | null;
};

// Zeilen aus deiner Datenbank (Drizzle, Prisma, reines SQL) in die Form des Blocks
export function toCrmDeal(row: DealRow): CrmDeal {
  return {
    id: row.id,
    stageId: row.stage_id,
    title: row.title,
    amount: row.amount_cents / 100,
    currency: row.currency,
    probability: row.probability ?? undefined,
    company: { name: row.company_name },
    companyId: row.company_id,
    owner: { id: row.owner_id, name: row.owner_name },
    expectedCloseDate: row.close_date ?? undefined,
    stageEnteredAt: row.stage_entered_at,
    lostReason: row.lost_reason ?? undefined,
    href: `/deals?deal=${row.id}`,
  };
}
```

Die Seite ist eine Server-Komponente. Sie prüft die Session, lädt alles parallel und liest `?deal=`, damit ein Link das Sheet eines Deals öffnen kann. Deine Query-Helfer (`listDeals`, `listOwners`, `listCompanies`) geben `CrmDeal[]`, `CrmOwner[]` und `CrmPipelineCompany[]` zurück. Gib jeder Firma ein `href` wie `/clients/<id>`, dann verlinkt das Deal-Sheet auf ihren Datensatz.

```tsx
// app/(admin)/deals/page.tsx
import { verifySession } from "@/server/dal";
import { PIPELINES, listCompanies, listDeals, listOwners } from "@/server/crm";
import { DealsView } from "./deals-view";

export default async function DealsPage({ searchParams }: { searchParams: Promise<{ deal?: string }> }) {
  const { userId } = await verifySession();
  const [{ deal }, deals, owners, companies] = await Promise.all([searchParams, listDeals(), listOwners(), listCompanies()]);
  return (
    <DealsView
      pipelines={PIPELINES}
      deals={deals}
      owners={owners}
      companies={companies}
      currentUserId={userId}
      openDeal={deal}
    />
  );
}
```

Der Block ist eine Client-Komponente, also stecken die Callbacks in einer kleinen View. Jeder Handler gibt das Promise der Server Action zurück: Die Karte zeigt einen Spinner, solange sie läuft, und wird es abgelehnt, springt sie mit einem Toast zurück.

Zwei Details in den Handlern übersieht man leicht. `onDealMove` feuert auch, wenn jemand Karten innerhalb einer Spalte umsortiert (`move.from.id === move.to.id`), deshalb überspringt die View diese Fälle. Sonst würde jedes Umsortieren `stage_entered_at` zurücksetzen und damit den Hinweis auf veraltete Deals und das Datum des Gewinns löschen. Und `onDealSave` liefert eine Notiz aus dem Sheet als neuesten `activity`-Eintrag mit `kind: "note"`. Ihre ID ist stabil, also speicherst du Notizen per Upsert über diese ID: Ein späteres Speichern desselben Deals schickt dieselbe Notiz noch einmal.

```tsx
// app/(admin)/deals/deals-view.tsx
"use client";

import {
  CrmPipelinePage,
  type CrmDeal,
  type CrmOwner,
  type CrmPipeline,
  type CrmPipelineCompany,
} from "@/components/blocks/crm-pipeline-page";
import { createDeal, deleteDeal, moveDeal, saveDeal } from "./actions";

type Props = {
  pipelines: CrmPipeline[];
  deals: CrmDeal[];
  owners: CrmOwner[];
  companies: CrmPipelineCompany[];
  currentUserId: string;
  openDeal?: string;
};

export function DealsView({ pipelines, deals, owners, companies, currentUserId, openDeal }: Props) {
  return (
    <CrmPipelinePage
      height="fill"
      pipelines={pipelines}
      defaultPipeline={pipelines[0]?.id}
      defaultDeals={deals}
      owners={owners}
      companies={companies}
      currentUserId={currentUserId}
      defaultOpenDeal={openDeal}
      onDealMove={(move) =>
        // auch ein Umsortieren in einer Spalte landet hier; überspringen, damit stageEnteredAt stimmt
        move.from.id === move.to.id ? undefined : moveDeal(move.deal.id, move.to.id, move.lostReason)
      }
      onDealSave={(deal) => {
        const latest = deal.activity?.[0];
        const note = latest?.kind === "note" ? latest : undefined;
        return saveDeal({
          id: deal.id,
          title: deal.title,
          amount: deal.amount,
          ownerId: deal.owner?.id,
          expectedCloseDate: deal.expectedCloseDate,
          note: note ? { id: note.id, text: note.description ?? "" } : undefined,
        });
      }}
      onDealCreate={(deal) =>
        createDeal({
          title: deal.title,
          stageId: deal.stageId,
          amount: deal.amount,
          companyId: deal.companyId,
          ownerId: deal.owner?.id,
          expectedCloseDate: deal.expectedCloseDate,
        })
      }
      onDealDelete={(deal) => deleteDeal(deal.id)}
    />
  );
}
```

Schick IDs und einfache Felder an die Actions, nie den ganzen Deal: Eine Server Action ist ein öffentlicher Endpunkt, also nimmt sie nur an, was sie validiert. Hier die Verschiebung, die am häufigsten läuft:

```ts
// app/(admin)/deals/actions.ts
"use server";

import { z } from "zod";
import { verifySession } from "@/server/dal";
import { PIPELINES, updateDeal } from "@/server/crm";

const stageIds = PIPELINES.flatMap((pipeline) => pipeline.stages.map((stage) => stage.id));

const Move = z.object({
  dealId: z.string().min(1),
  stageId: z.string().refine((id) => stageIds.includes(id), "Unbekannte Phase"),
  lostReason: z.string().max(200).optional(),
});

export async function moveDeal(dealId: string, stageId: string, lostReason?: string) {
  const { userId } = await verifySession();
  const move = Move.parse({ dealId, stageId, lostReason });
  await updateDeal(userId, move.dealId, {
    stage_id: move.stageId,
    lost_reason: move.lostReason ?? null,
    stage_entered_at: new Date().toISOString(),
  });
}
```

`saveDeal`, `createDeal` und `deleteDeal` folgen demselben Muster (`saveDeal` validiert die optionale `note` mit `z.object({ id: z.string(), text: z.string().max(5000) })`). `createDeal` gibt `toCrmDeal(row)` zurück, damit der gespeicherte Deal den Entwurf auf dem Board ersetzt. Das Board bleibt unkontrolliert (`defaultDeals`), also musst du die Seite nach einer Verschiebung nicht revalidieren: Der optimistische State zeigt das Ergebnis schon.

> Live-Demo (CRM Pipeline Page): Zieh Online bookings in eine andere Phase: Diese Demo lässt das erste Speichern absichtlich scheitern, also springt die Karte mit einem Toast zurück. Zieh danach einen Deal auf Lost und wähl einen Grund, oder klick auf eine Karte, um ihr Sheet zu öffnen. Probier sie unter [CRM Pipeline Page](https://wingo-ui.com/components/crm-pipeline-page) aus und installier sie mit `npx wingo-ui@latest add crm-pipeline-page`.

### Wie funktioniert die gewichtete Prognose?

Die gewichtete Summe einer Phase ist die Summe aus Betrag mal Wahrscheinlichkeit über ihre Deals. Die Wahrscheinlichkeit kommt von der Phase, außer der Deal setzt eine eigene `probability`, die sie überschreibt. Gewonnene Deals zählen voll, verlorene null. Mit den Phasen von oben:

| Deal | Phase | Betrag | Wahrscheinlichkeit | Gewichtet |
|---|---|---|---|---|
| Kundenportal | Qualifiziert | $42,000 | 10% (Phase) | $4,200 |
| Website-Redesign | Angebot gesendet | $18,000 | 50% (Phase) | $9,000 |
| Supportvertrag | Verhandlung | $30,000 | 60% (Deal überschreibt) | $18,000 |
| **Offene Pipeline** | | **$90,000** | | **$31,200** |

Die Zusammenfassung hat vier Kacheln: offene Pipeline, gewichtete Prognose, im Zeitraum gewonnen (standardmäßig der aktuelle Monat, datiert über `stageEnteredAt`) und die Abschlussquote, also gewonnene Deals geteilt durch gewonnene plus verlorene im selben Zeitraum. Bei der Deal Pipeline und bei `PipelineSummary` ersetzt `showWeighted={false}` die gewichtete Kachel durch den durchschnittlichen offenen Deal. Deals in einer anderen Währung werden getrennt summiert, mit einem Hinweis, und nie umgerechnet. Ohne einen Wechselkurs, den du selbst kontrollierst, ist das die einzige ehrliche Voreinstellung.

Die Wahrscheinlichkeiten pro Phase sind die Schwachstelle. Leg sie anhand deiner eigenen abgeschlossenen Deals fest (wie viele der Deals, die „Angebot gesendet“ erreicht haben, wurden gewonnen?) und prüf sie jedes Quartal neu. Behandle die Zahlen oben als Platzhalter, bis deine eigene Historie sie ersetzt.

Um dieselbe Prognose auf die Startseite des Dashboards zu bringen, renderst du die Zusammenfassung allein. Sie ist eine Client-Komponente, die einfache Daten bekommt, also kann eine Server-Seite sie direkt rendern. Für eigene Kacheln liefert `computePipelineStats(deals, stages)` aus derselben Datei die Zahlen hinter der Zusammenfassung. Die Datei beginnt mit `"use client"`, also ruf die Funktion aus Client-Code auf: Eine Server-Komponente, die sie importiert, bekommt eine Client-Referenz statt einer Funktion.

```tsx
// app/(admin)/page.tsx (Auszug)
import { PipelineSummary } from "@/components/ui/deal-pipeline";
import { PIPELINES, listDeals } from "@/server/crm";

export default async function HomePage() {
  const deals = await listDeals();
  return <PipelineSummary deals={deals} stages={PIPELINES[0].stages} currency="USD" locale="en-US" />;
}
```

## Wie baust du die Firmenseite?

Die [CRM Contact Page](https://wingo-ui.com/components/crm-contact-page) bekommt einen Datensatz: die Firma, ihre Kontakte, Deals, Aktivitäten, Dateien und Notizen. Für alles, was du weglässt, greift sie auf Beispieldaten zurück, also gib jeden Teil mit, auch `people` und `currentUser`.

```tsx
// app/(admin)/clients/[id]/page.tsx
import { notFound } from "next/navigation";
import { verifySession } from "@/server/dal";
import { PIPELINES, getClientRecord, getTeam } from "@/server/crm";
import { ClientView } from "./client-view";

export default async function ClientPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const { userId } = await verifySession();
  const [record, team] = await Promise.all([getClientRecord(id), getTeam()]);
  if (!record) notFound();
  const me = team.find((person) => person.id === userId);
  return <ClientView record={record} stages={PIPELINES[0].stages} team={team} me={me} />;
}
```

`getClientRecord` gibt ein `CrmContactSample` (`{ company, contacts, deals, activity, files, notes }`) zurück, dazu seine Position in der Kundenliste. Gib jedem Deal `href: "/deals?deal=<id>"`, dann öffnet eine Zeile im Tab Deals das Sheet dieses Deals auf dem Board.

```tsx
// app/(admin)/clients/[id]/client-view.tsx
"use client";

import type { CommentAuthor } from "@/components/ui/comment-thread";
import { CrmContactPage, type CrmContactSample, type CrmDealStage } from "@/components/blocks/crm-contact-page";
import { addDeal, deleteNote, saveNote, setTags } from "./actions";

type Props = {
  record: CrmContactSample & { index: number; total: number; previousId?: string; nextId?: string };
  stages: CrmDealStage[];
  team: CommentAuthor[];
  me?: CommentAuthor;
};

export function ClientView({ record, stages, team, me }: Props) {
  const { company, index, total, previousId, nextId, ...parts } = record;
  return (
    <CrmContactPage
      {...parts}
      company={company}
      stages={stages}
      people={team}
      currentUser={me}
      pager={{
        index,
        total,
        previousHref: previousId ? `/clients/${previousId}` : undefined,
        nextHref: nextId ? `/clients/${nextId}` : undefined,
      }}
      previousShortcut="alt+arrowleft"
      nextShortcut="alt+arrowright"
      listHref="/clients"
      invoiceHref={`/invoices/new?client=${company.id}`}
      contactHref={`/contacts/new?client=${company.id}`}
      onNoteSave={(note) => saveNote(company.id, { id: note.id, content: note.content, pinned: note.pinned })}
      onNoteDelete={(note) => deleteNote(company.id, note.id)}
      onTagsChange={(tags) => setTags(company.id, tags)}
      onDealCreate={(values) => addDeal(company.id, values)}
    />
  );
}
```

Zwei Dinge erledigst du auf dem Server. Notizen kommen als HTML aus dem Rich-Text-Editor, also bereinige sie, bevor du sie speicherst oder renderst. Und gib `onUpload` mit, sobald du einen Dateispeicher hast, denn ohne diesen Callback simuliert der Tab Files den Upload nur.

> Live-Demo (CRM Contact Page): Öffne den Tab Deals, dann den Tab Notes, und speichere eine Notiz mit Cmd + Enter. Drück Alt + Pfeil rechts, um zum nächsten Kunden zu springen, oder scroll nach unten, bis die kompakte Leiste Name und Hauptaktion sichtbar hält. Probier sie unter [CRM Contact Page](https://wingo-ui.com/components/crm-contact-page) aus und installier sie mit `npx wingo-ui@latest add crm-contact-page`.

## Wie funktioniert das CRM auf dem Smartphone?

Auf dem Smartphone zeigt das Board jeweils eine Phase, das Ziehen startet mit einem langen Druck, und alle Menüs, Sheets und Dialoge öffnen sich von unten. Im Detail:

- **Board.** Jeweils eine Phase auf 85% der Breite, die nächste schaut am Rand herein, und über eine Reihe Chips springst du zwischen den Phasen. Ein Deal hebt sich erst nach einem langen Druck von 250ms an, damit ein Wischen weiter scrollt. Das Menü ⋯ (Move to, Mark as won, Mark as lost) öffnet sich als Bottom Sheet.
- **Zusammenfassung.** Ein Raster aus 2 x 2 Kacheln, das sich mit einem Tipp auf seinen Chevron zu einer Zeile zusammenklappt.
- **Deal-Sheet und Dialoge.** Das Sheet wird zu einem hohen Bottom Sheet, der Pipeline-Wechsler und der Dialog für den Verlustgrund öffnen sich von unten, und aus New deal wird ein +-Button.
- **Firmenseite.** Die Aktionen im Header klappen in ⋯ zusammen, das Seitenpanel wird zum Tab Details, und die Deal-Tabelle wird zu Karten.

Wie das Touch-Ziehen hinter dem Board funktioniert, erklärt unser [Tutorial zu dnd kit und Touch-Drag auf dem Smartphone](https://wingo-ui.com/de/blog/dnd-kit-mobile-touch-drag). Das Board selbst, mit WIP-Limits und Verschieben per Tastatur, behandelt der [Guide zum React Kanban Board](https://wingo-ui.com/de/blog/react-kanban-board).

## Kannst du das Deal Pipeline Kanban allein nutzen?

Ja. Die CRM Pipeline Page ist der volle Screen mit Header, Filtern, Deal-Sheet und Dialog für neue Deals. Die Deal Pipeline ist die Sales Pipeline UI, die du in React-Apps überall einbetten kannst: das Board mit der Zusammenfassung, der Listenansicht und den Chips für Verantwortliche, und nichts drumherum. Nimm sie für ein deal pipeline kanban in einem Kundenportal oder auf einer Projektseite, wo ein ganzer CRM-Screen zu viel wäre.

Die Props, die zählen, wenn du sie allein nutzt:

- `stages` plus `defaultValue` (unkontrolliert) oder `value` mit `onValueChange` (kontrolliert). `onValueChange` bekommt das neue Array und den `DealMove`, der bei neu angelegten Deals und bei Rollbacks undefined ist.
- `onDealMove` mit demselben Vertrag wie auf der Seite: Gib ein Promise zurück, solange die Karte speichern soll, lass es fehlschlagen (reject), damit sie zurückspringt, oder gib `false` zurück, um die Verschiebung abzulehnen.
- `onDealClick`, um dein eigenes Panel zu öffnen. Ohne diesen Callback folgt eine Karte mit `href` dem Link.
- `persistKey`, um Ansicht, Filter nach Verantwortlichen und eingeklappte Spalten pro Person zu merken.
- `getDealActions` für zusätzliche Einträge im Menü ⋯ nach Move to, Mark as won und Mark as lost, und `renderCard` mit den Teilen von `DealCard`, wenn die Standardkarte die falschen Felder zeigt.

> Live-Demo (Deal Pipeline): Wechsel in der Toolbar zu List, um dieselben Deals nach Phase gruppiert zu sehen, dann drück einen Chip für Verantwortliche und schau, wie die Zusammenfassung dem Filter folgt. Probier sie unter [Deal Pipeline](https://wingo-ui.com/components/deal-pipeline) aus und installier sie mit `npx wingo-ui@latest add deal-pipeline`.

Für ein komplettes react crm dashboard template mit KPIs, Tabellen und Einstellungen rund um diese Screens startest du mit dem [Next.js Admin-Dashboard-Tutorial](https://wingo-ui.com/de/blog/nextjs-admin-dashboard-tutorial). Die [Kategorie Build](https://wingo-ui.com/de/blog/category/build) zeigt die anderen Screens Schritt für Schritt.

## Was baust du als Nächstes?

Installier die Blöcke mit `npx wingo-ui@latest add crm-pipeline-page crm-contact-page`, schließ `listDeals` an deine Datenbank an und verdrahte zuerst `moveDeal`, denn diese Action nutzen die Leute am häufigsten. Die CRM-Blöcke sind Pro ([Preise](https://wingo-ui.com/de/pricing)). Wenn du mit Claude Code oder Cursor baust, verbinde den [MCP-Server von Wingo UI](https://wingo-ui.com/docs/mcp), damit der Agent die echten Props jedes Blocks liest, statt sie zu raten.

## Komponenten in diesem Artikel

- [CRM Pipeline Page](https://wingo-ui.com/components/crm-pipeline-page) (Pro): A React CRM pipeline page: pipeline switcher, search and filters, a deals board or list with totals, a deal sheet and a new deal dialog. Installation: `npx wingo-ui@latest add crm-pipeline-page`
- [CRM Contact Page](https://wingo-ui.com/components/crm-contact-page) (Pro): A React CRM contact page: a company record with facts and actions, tabs for overview, activity, deals, files and notes, and a details panel. Installation: `npx wingo-ui@latest add crm-contact-page`
- [Deal Pipeline](https://wingo-ui.com/components/deal-pipeline) (Pro): A React sales pipeline board: deal stages with weighted totals, a forecast summary, drag between stages with a lost reason, a list view. Installation: `npx wingo-ui@latest add deal-pipeline`
- [Kanban](https://wingo-ui.com/components/kanban) (Pro): A React kanban board for tasks, orders and pipelines: drag cards with mouse, touch or keyboard, WIP limits, inline add and a phone layout. Installation: `npx wingo-ui@latest add kanban`

## Häufige Fragen

### Gibt es ein kostenloses React CRM Template?

Ja. Refine veröffentlicht ein CRM-Beispiel auf Basis von React, Ant Design und einer GraphQL-API (Stand Oktober 2026 ist die volle Version in die Enterprise Edition gewandert, eine minimale Community-Version unter MIT-Lizenz bleibt), und Twenty ist ein komplettes Open-Source-CRM, größtenteils unter AGPLv3, mit eigenem Backend. Die CRM-Blöcke von Wingo UI aus diesem Tutorial sind Pro.

### Wie berechnet man eine gewichtete Pipeline-Prognose?

Multiplizier den Betrag jedes offenen Deals mit seiner Gewinnwahrscheinlichkeit und addier die Ergebnisse. Die Wahrscheinlichkeit kommt meist von der Phase des Deals, und ein Deal kann sie überschreiben; gewonnene Deals zählen voll, verlorene mit null.

### Woher sollen die Wahrscheinlichkeiten pro Phase kommen?

Aus deinen eigenen abgeschlossenen Deals: pro Phase der Anteil der Deals, die sie erreicht haben und später gewonnen wurden. Ohne Historie startest du mit groben Schätzungen und kalibrierst neu, sobald ein paar Monate abgeschlossener Deals vorliegen.

### Kann ich ein React CRM Template mit meinem eigenen Backend nutzen?

Ja, wenn das Template nur die UI liefert. Die Blöcke von Wingo UI bekommen ihre Daten über Props und melden jede Änderung über Callbacks wie onDealMove und onNoteSave, also funktionieren sie mit Server Actions, einer REST-API oder GraphQL.

### Wie funktioniert ein Board für die Sales-Pipeline auf dem Smartphone?

Zeig jeweils eine Phase und lass die nächste am Rand hereinschauen, ergänz Chips, um zwischen den Phasen zu springen, und heb Karten erst nach einem langen Druck an, damit ein Wischen weiter scrollt. Für alle, die nicht ziehen wollen, gibt es ein Menü „Move to“ im Bottom Sheet.

---

Quelle: https://wingo-ui.com/de/blog/react-crm-template
