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
// 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
// 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
// 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[]
}