Teamwork Projects Integration
How the account view’s Projects panel sources, filters, and displays Teamwork project delivery state for a subscriber.
Data flow
Section titled “Data flow”Teamwork API ──(ingest)──▶ BigQuery: fcr_operations.teamwork_project_tasks fcr_operations.teamwork_project_milestones │ ▼ Worker: /dashboard-teamwork-projects (worker/src/handlers/teamwork-projects.js) │ ▼ Frontend: ProjectsPanel.jsx (src/components/panels/ProjectsPanel.jsx)Teamwork is no longer called live — everything is served from BigQuery tables that are refreshed by an ingestion job. The worker composes two parallel BQ queries per request and shapes the flat rows into a project → tasklist → task hierarchy the UI can render.
BigQuery source tables
Section titled “BigQuery source tables”fcr_operations.teamwork_project_tasks
Section titled “fcr_operations.teamwork_project_tasks”One row per task, or a project-only row if the project has no tasklists yet. Currently ~47.5k rows spanning 3,788 distinct subscribers / 3,788 projects / 2,717 tasklists / 45,164 tasks.
Key columns used:
- Project:
project_id,project_name,project_display_name,project_url,project_status,project_sub_status,project_company_name - Tasklist:
tasklist_id,tasklist_name,tasklist_description,tasklist_status,tasklist_complete - Task:
task_id,task_name,task_completed,task_due_date,task_completed_at,task_progress,task_priority,task_assignees,task_updated_at - Scope:
subscriber_id
fcr_operations.teamwork_project_milestones
Section titled “fcr_operations.teamwork_project_milestones”One row per milestone, joined on project_id. ~10,866 rows across 3,672 projects. Only 64 milestones are marked complete in the source — the staging job keeps the current open set plus a trailing completed tail. Columns used: project_id, milestone_id, milestone_name, milestone_completed, milestone_deadline.
Worker endpoint — /dashboard-teamwork-projects
Section titled “Worker endpoint — /dashboard-teamwork-projects”Request: GET /dashboard-teamwork-projects?subscriberId=<id>
Auth: x-api-key header (same as every other dashboard endpoint).
What it pulls
Section titled “What it pulls”The handler runs both queries in parallel (Promise.allSettled) so a milestones failure does not break the projects payload:
-- tasksSELECT project_id, project_name, project_display_name, project_status, project_sub_status, project_url, project_company_name, tasklist_id, tasklist_name, tasklist_description, tasklist_status, tasklist_complete, task_id, task_name, task_completed, task_due_date, task_completed_at, task_progress, task_priority, task_assignees, task_updated_atFROM fcr_operations.teamwork_project_tasksWHERE subscriber_id = @subscriberIdORDER BY project_id, tasklist_id, task_due_date;
-- milestonesSELECT project_id, milestone_id, milestone_name, milestone_completed, milestone_deadlineFROM fcr_operations.teamwork_project_milestonesWHERE subscriber_id = @subscriberId;Note: no status filter is applied at the SQL layer — the subscriber’s entire Teamwork footprint is returned. In practice the upstream BQ staging job already excludes archived projects, so 99% of rows are project_status='active' (sub-statuses: current for 3,353 projects, late for 433; inactive/current carries 2 edge cases today).
What it returns
Section titled “What it returns”The worker groups the flat rows into a nested structure:
{ "count": 2, "projects": [ { "id": 12345, "name": "Client Delivery Project", "displayName": "Acme Ltd — Current Program", "projectUrl": "https://fcrmedia.teamwork.com/#/projects/12345", "status": "active", "subStatus": "current", // 'current' or 'late' "companyName": "Acme Ltd", "tasklists": [ { "id": 98765, "name": "Website Build", "status": "new", // 'new' | 'reopened' | null "description": "**Product:** ... **Price:** ... **Rep Name:** ...", "totalTasks": 14, "completedTasks": 9, "overdueTasks": 1, "progressPct": 64, "lastActivity": "2026-04-02T00:00:00.000Z", "tasks": [ { "id": 555, "name": "Go-live approval", "completed": false, "isOverdue": true, "dueDate": "2026-04-01", "assignees": ["Jane Doe"] } ] } ], "taskStats": { "totalTasks": 23, "completedTasks": 14, "progressPct": 61, "overdueTasks": 1 }, "milestones": [ { "name": "Go-live", "isLate": true } ] } ]}Per-tasklist rollups computed by the worker
Section titled “Per-tasklist rollups computed by the worker”For each tasklist the handler computes:
totalTasks,completedTasks,overdueTasksprogressPct = round(completed / total * 100)lastActivity— max taskdue_datein the tasklist (used as a proxy for “latest scheduled work”)isOverdueper task —!completed && due_date < now()
Project-level taskStats is the sum of the tasklist rollups. Milestones are attached via a {project_id → milestone[]} lookup and each gets an isLate flag (!completed && deadline < now()).
Population of tasks pulled per project
Section titled “Population of tasks pulled per project”The worker does not thin the task list — every task row returned by BQ for the subscriber is included in the response. Filtering is the UI’s job:
- Backend scope: all tasks on all tasklists of all
activeprojects for the subscriber, regardless of task status (open, completed, overdue, reopened). - Overdue detection: computed at request time against
now(), not materialised in BQ, so the flag is always current. - Tasklist without tasks: the project-only row still produces a project entry with an empty
tasklistsarray — this is how we surface newly created projects that haven’t been broken down yet.
Observed tasklist status distribution today: new ≈ 28,949, reopened ≈ 16,492, null ≈ 2,128.
Frontend — ProjectsPanel.jsx
Section titled “Frontend — ProjectsPanel.jsx”The panel renders one ProjectCard per project. Relevance decisions live here:
Headline stats (top-right of each project card)
Section titled “Headline stats (top-right of each project card)”- Products — count of tasklists on the project
- Tasks —
completed/totalacross all tasklists - Progress — overall
progressPct - Overdue — only shown (and highlighted red) when
overdueTasks > 0
Milestone summary
Section titled “Milestone summary”Only late milestones are surfaced (isLate === true). The panel shows a red pill with a count and the first 3 late milestone names (+N more if there are more). Completed / on-track milestones are intentionally hidden — the goal is a “what’s at risk” view, not a full milestone log.
Tasklist rows
Section titled “Tasklist rows”Each tasklist renders as a row with:
- Left border colour keyed off
tasklist.status(new=blue,reopened=amber,completed/closed=grey) — or red if any task is overdue, which overrides status colour - Status pill, product / rep / price / billing parsed out of the markdown
tasklist.description - Completed/total counts, overdue badge, “Last: Nd ago” relative time from
lastActivity - Mini progress bar
Clicking a row expands the individual task list (status dot, strikethrough for completed, red overdue badge, assignees, due date).
Filter pills
Section titled “Filter pills”If the project has more than one distinct tasklist status, a pill row lets the user filter to new / reopened / etc. Default is “All”. The filter is purely client-side against tasklist.status.
What is intentionally not shown
Section titled “What is intentionally not shown”- Completed milestones — only late ones surface in the summary strip (full list is still in the payload if we ever want a “history” view)
- Archived / closed projects — the upstream staging job already excludes them
- Task descriptions / comments — not fetched; the panel is a status dashboard, not a task editor
- Time logs / budget burn — out of scope for this build
- Priority / progress % — present in the payload (
task_priority,task_progress) but not rendered; room to add if asked
Caching
Section titled “Caching”Not in TTL_CONFIG — falls back to the worker’s DEFAULT_TTL of 6h in KV. Cache key is SHA-256(subscriberId=<id>), prefixed dash:dashboard-teamwork-projects:. To force a refresh after a backfill, either append a dummy query param or purge the KV key directly (see docs/HANDOFF.md for the wrangler KV commands).
worker/src/handlers/teamwork-projects.js— endpoint + shapingsrc/components/panels/ProjectsPanel.jsx— UI + filtering + description parsingsrc/screens/AccountView.jsx— panel mount pointsrc/hooks/useDashboardData.js— fetch wiring