/portal/reports/jobs is the report most contractors have never seen because it's a math problem nobody had time to solve manually: take revenue, subtract parts + labor + other costs, divide by revenue, that's your margin. Repeat for every job. Average by service type. Now you know which work to push and which to reprice.
What the math is
| Revenue | Sum of line_items on invoices for this customer within ±14 days of the appointment's completed_at. Proximity matching since invoices don't have a hard appointment_id FK yet. |
| Parts | Sum of expenses tagged appointment_id with category in (materials, parts, permits). |
| Labor | Sum of time_entries.cost_cents for the appointment. Each time entry snapshots the tech's hourly_rate_cents at clock-in so historical math doesn't drift if rates change. |
| Other costs | Other expense categories tagged to the appointment — fuel allocation, subcontractor labor, etc. |
| Net | Revenue − Parts − Labor − Other. |
| Margin | Net ÷ Revenue, expressed as a percentage. |
What to do with it
- 1Sort by margin: low to high
The first 5 rows are your money-losers. Either reprice (most common — flat-rate pricing book undercharges for emergency callouts), drop the service entirely, or assign your fastest tech.
- 2Read the service-type panel
Right side of the page shows average margin per service. "Drain cleanings 71% / AC tune-ups 58% / Water heater installs 41%" lets you push more drain calls in your marketing instead of more water heaters.
- 3Find your billing leaks
If a job shows revenue $0 with $400 of parts + 3 hours of labor, you forgot to invoice. The report flags it loudly with a negative net column.
Per-job tile on the job page
Every completed appointment shows the same numbers inline at /portal/tech/[id]. Owner + billing roles see it; tech role doesn't (it's their labor on the line, surfacing it to them is a culture call). The tile links straight to /portal/reports/jobs for the full grid.
Setting tech hourly rates
Settings → Team → click a tech → set hourly rate. This is the FULLY-LOADED cost (payroll + payroll tax + truck overhead), not the pay rate. Rates are stored in cents on the tech record and snapshotted onto every time entry at clock-in. Updating the rate doesn't change past entries — they keep the rate that was in effect when the work was done.
Some rows show a small alert icon next to the revenue column — that means the matched invoice covered multiple appointments (typical for monthly commercial customers). The dollar amount got split evenly across the appointments it could match. Treat those numbers as approximate, not exact.
Jobs without an attached invoice (free callbacks, warranty work) show $0 revenue and a negative net = parts + labor cost only. That's correct — those jobs cost you money. Surface them so you know how much warranty work is actually costing.