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
| Field | Description |
|---|---|
date | Timesheet completion date |
project | Project name |
person | Full name of the person |
actual_hours | Hours actually worked |
invoiced_hours | Billed hours |
rate | Hourly rate from project_member.in_net_hourly_rate |
amount | Computed: invoiced_hours * rate |
experience | Experience 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 thequeryFn. This was a deliberate fix — includingselectedProjectsin 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 nameformat—csvorxlsxcolumns— array of{ field, label }objectsgroup_by—none,project, orweekinclude_totals— boolean
Hooks: useTimesheetExportTemplates(), useCreateTimesheetExportTemplate(), useDeleteTimesheetExportTemplate()
Key Files
| File | Purpose |
|---|---|
web/src/components/crm/TimesheetExportWizard.tsx | Wizard UI (467 lines) |
web/src/lib/hooks/useTimesheetExport.ts | Data fetching + template CRUD hooks |
web/src/lib/timesheet-export.ts | Export 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 theuseMonthlyHourspattern. - 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.
Related
- levandor-crm - Project overview
- debugging-log-crm - Bug details for the fixes above
- agent-context-crm - Agent quick reference