Features

Client Management

Enrich client profiles with avatars and social links, track referral chains, monitor lead sources, and measure per-client profitability.

Client Enrichment

When a client is created (either manually or through lead conversion), the app enriches their profile with additional data:

Avatar

Fetched from Gravatar based on the client email. Falls back to initials if unavailable.

LinkedIn URL

Optional field to link directly to the client's LinkedIn profile for quick reference.

Domain

Automatically extracted from the email address (e.g., acme.com from jane@acme.com).

Company Name

Free-text field used for grouping and searching. Displayed on invoices.

Referral Chain Tracking

The CRM supports multi-level referral tracking. Each client can have a "referred by" relationship pointing to another client, forming a chain. This lets you see which clients bring you the most downstream revenue.

Sarah (original) → referred Mike → referred Lisa → referred Tom

All downstream revenue is attributed back through the chain

  • *Referral chains are visualized in the Revenue Sankey chart on the dashboard
  • *Each client profile shows who referred them and who they referred
  • *Self-referential relationship in Prisma using the same Client model

Source Tracking

Every client has a source field that records how they found you. Common values include:

ReferralUpworkLinkedInCold EmailWebsiteConferenceTwitter/X

Source data flows into the Dashboard Sankey chart, showing you which acquisition channels generate the most revenue.

Per-Client Profitability

The client detail page shows a profitability summary calculated from all paid invoices and logged time entries. Key metrics include:

  • *Total Revenue — sum of all paid invoices for this client
  • *Total Hours — sum of all time entries across all projects
  • *Effective Hourly Rate — total revenue divided by total hours, showing what you actually earned per hour
src/lib/profitability.ts
// Per-client profitability calculation
function calculateProfitability(client: ClientWithRelations) {
  const totalRevenue = client.invoices
    .filter((inv) => inv.status === "PAID")
    .reduce((sum, inv) => sum + inv.amount, 0);

  const totalHours = client.projects
    .flatMap((p) => p.timeEntries)
    .reduce((sum, entry) => sum + entry.hours, 0);

  const effectiveRate = totalHours > 0
    ? totalRevenue / totalHours
    : null;

  return { totalRevenue, totalHours, effectiveRate };
}

Data Model

prisma/schema.prisma
// Client model with enrichment fields
model Client {
  id            String    @id @default(cuid())
  name          String
  email         String    @unique
  company       String?
  avatarUrl     String?   // Auto-fetched or uploaded
  linkedinUrl   String?
  domain        String?   // Extracted from email
  source        String?   // "referral", "upwork", "cold", etc.
  referredById  String?   // Links to another Client
  referredBy    Client?   @relation("Referrals", fields: [referredById])
  referrals     Client[]  @relation("Referrals")
  projects      Project[]
  invoices      Invoice[]
  createdAt     DateTime  @default(now())
}

Related pages