Timesheet Export

3-step wizard for exporting timesheet data to CSV or Excel with configurable columns, grouping, and saveable templates.

Wizard Steps

Step 1: Scope

  • Template selection — load a saved template to pre-fill columns, format, and grouping
  • Person — select which person’s timesheets to export (defaults to current user)
  • Date range — presets (This Week, This Month, Last Month) or custom date range
  • Project filter — checkbox list of projects the person is a member of; empty = all projects

Step 2: Columns & Format

  • Format — CSV or Excel (xlsx)
  • Group by — None, Project, or Week
  • Column builder — drag-and-drop reorder (dnd-kit), add/remove columns, edit labels
  • Include totals — toggle for summary row
  • Save as template — optionally save current config for reuse
  • Delete template — remove a previously saved template

Step 3: Preview & Download

  • Summary line: date range and row count
  • Preview table showing first 10 rows with configured columns
  • Download button generates the file client-side

Available Export Fields

FieldDescription
dateTimesheet completion date
projectProject name
personFull name of the person
actual_hoursHours actually worked
invoiced_hoursBilled hours
rateHourly rate from project_member.in_net_hourly_rate
amountComputed: invoiced_hours * rate
experienceExperience level annotation

Data Flow

useTimesheetExportData(personId, start, end)
    ├── timesheet table (filtered by person + date range)
    ├── project_member table (rates per project)
    └── person table (name)
         ↓
    Client-side join → TimesheetExportRow[]
         ↓
    Client-side filter by selectedProjects (useMemo)
         ↓
    buildExportRows() → headers + rows
         ↓
    downloadTimesheetCsv() or downloadTimesheetXlsx()

Query Design Decision

Project filtering is done client-side via useMemo, NOT in the queryFn. This was a deliberate fix — including selectedProjects in the query would cause unnecessary refetches. The query fetches all timesheets for the date range, and the component filters locally. See Timesheet Export Project Filter Cache Bug (2026-04-01).

Templates

Saved to timesheet_export_template table. Each template stores:

  • name — display name
  • formatcsv or xlsx
  • columns — array of { field, label } objects
  • group_bynone, project, or week
  • include_totals — boolean

Hooks: useTimesheetExportTemplates(), useCreateTimesheetExportTemplate(), useDeleteTimesheetExportTemplate()

Key Files

FilePurpose
web/src/components/crm/TimesheetExportWizard.tsxWizard UI (467 lines)
web/src/lib/hooks/useTimesheetExport.tsData fetching + template CRUD hooks
web/src/lib/timesheet-export.tsExport logic: buildExportRows(), downloadTimesheetCsv(), downloadTimesheetXlsx()

Bugs Fixed

  • Empty data on inner join (bfabb45): Original query joined timesheet + project_member in a single Supabase select, which returned 0 rows. Split into 3 parallel queries matching the useMonthlyHours pattern.
  • Project untick from “all” state (bfabb45): Unchecking a project from the implicit “all selected” state (empty array) now expands to all project names first, then removes the unchecked one.
  • Stale preview from cache (9d5acd3): Project IDs were missing from queryKey, so TanStack Query served cached unfiltered data. Moved filtering to client-side useMemo.