Features

Project Tracking

Manage fixed-fee and hourly projects, log time entries, extend budgets when scope grows, and track every project through its lifecycle.

Fixed vs. Hourly Projects

The CRM supports two project billing models. Choose the type when creating a project — it determines how budgets and invoices are calculated.

Fixed Fee

  • * One agreed-upon budget for the entire project
  • * Invoiced in milestones or on completion
  • * Budget tracked as a single number
  • * No hourly rate or time entries required

Hourly

  • * Budget acts as a cap (e.g., "not to exceed $10K")
  • * Hourly rate set per project
  • * Time entries logged against the project
  • * Uninvoiced hours tracked and alerted

Time Entries

For hourly projects, you log time entries with a description, hours, and date. Each entry is marked as invoiced or uninvoiced. The system warns you when uninvoiced hours accumulate past a threshold.

  • *Log entries from the project detail page or the quick-add bar
  • *Entries are automatically linked to the project and client
  • *When an invoice is created, uninvoiced entries are grouped and marked as invoiced
  • *The dashboard shows total hours and effective hourly rate across all projects
prisma/schema.prisma
// Logging time on an hourly project
model TimeEntry {
  id          String   @id @default(cuid())
  projectId   String
  project     Project  @relation(fields: [projectId])
  description String
  hours       Float
  date        DateTime
  invoiced    Boolean  @default(false)
}

Budget Extensions

When a project's scope grows beyond the original agreement, you can extend the budget rather than creating a new project. This keeps all time entries, invoices, and history under a single project record.

  • *Budget is incremented, not replaced — the original amount is preserved in the history
  • *Project status changes to "Extended" automatically
  • *Optionally set a new end date for the extended scope
src/lib/actions/projects.ts
// Server action: extend a project budget
"use server";

export async function extendProjectBudget(
  projectId: string,
  additionalBudget: number,
  newEndDate?: Date
) {
  return prisma.project.update({
    where: { id: projectId },
    data: {
      budget: { increment: additionalBudget },
      status: "EXTENDED",
      ...(newEndDate && { endDate: newEndDate }),
    },
  });
}

Project Status Lifecycle

Projects move through four statuses. Transitions can be triggered manually or automatically (e.g., budget extension sets "Extended").

Active

Work is in progress. Time entries can be logged and invoices created.

Extended

Budget was increased beyond the original scope. Still accepting time entries.

Completed

All deliverables shipped. Final invoice sent. No more time entries.

Canceled

Project stopped before completion. Partial invoices may exist.

Active → Extended (optional) → Completed

A project can move to Canceled from any status

Data Model

prisma/schema.prisma
// Project types and status lifecycle
enum ProjectType {
  FIXED    // Flat fee, milestone-based
  HOURLY   // Billed per hour logged
}

enum ProjectStatus {
  ACTIVE      // Work in progress
  EXTENDED    // Past original deadline, scope expanded
  COMPLETED   // All deliverables shipped
  CANCELED    // Stopped before completion
}

model Project {
  id          String        @id @default(cuid())
  name        String
  clientId    String
  client      Client        @relation(fields: [clientId])
  type        ProjectType
  status      ProjectStatus @default(ACTIVE)
  budget      Float
  hourlyRate  Float?        // Only for HOURLY projects
  startDate   DateTime
  endDate     DateTime?
  timeEntries TimeEntry[]
  invoices    Invoice[]
}

Related pages