People Tool
Overview
Section titled “Overview”The People tool provides a directory of all active employees and an org chart visualization. Data is synced from Productive.io using their People API.
Features
Section titled “Features”- People directory — Searchable, sortable table of all active employees with avatar, name, title, email, and manager
- Person profile page — Full-page profile with six tabs: Details (Contact, Role, Personal, System), Employment (Tenure, Pay, Leave, Financial), Documents, Activity, Notes, and LinkedIn. Navigates via
?id=personId - Confidential notes — Timestamped log of confidential notes on person profiles with rich text (Tiptap editor), per-note visibility control (multi-select access levels, executive always included), and tiered access: executives view/add/edit/delete all, heads view/add for subtree, managers view/add for direct reports
- Org chart — Tree visualization showing the reporting hierarchy based on manager relationships
- Org chart peek sheet — Simplified quick-peek drawer when clicking a node in the org chart, showing Contact/System info, Activity tab, and a “View Profile” button
- Xero sync on profiles — Executives can link a person to their Xero employee record (with smart name/email matching) and trigger payroll data sync directly from the profile System card
- Productive sync — Manual sync button (executive only) pulls active people from Productive and matches them to app users by email
- User enrichment — Sync updates matched users’ job title and profile picture from Productive
- TTL-based refresh — Data auto-refreshes in the background after 6 hours
- LinkedIn Activities — View a person’s recent LinkedIn posts, comments, and reactions (fetched from Scrapin.io, 3 credits per fetch). Managers/executives add a LinkedIn URL to the Contact card, then manually trigger fetches. Cached in DB for free viewing
- LinkedIn Change Alerts — Subscribe to Scrapin webhooks that detect LinkedIn profile changes (title, company, headline, location, skills). Shows a timeline of detected changes. Requires webhook URL configuration on the Connections page
Toggle between two views using tabs:
- Directory (default) — Table with sortable columns (name, title, email) and manager lookup. Clicking a row navigates to the full profile page
- Org Chart — Hierarchical tree with collapsible nodes, showing first 2 levels expanded by default. Clicking a node opens a peek sheet with quick info and a link to the full profile
- Profile Page — Shown when
?id=personIdis present. Tabs are permission-scoped: Employment, Documents, and Activity require manager/head/executive access
Sync Behaviour
Section titled “Sync Behaviour”When an executive clicks “Sync”:
- Fetches all active people from Productive (paginated, max 200 per page)
- Upserts into the
peopletable, matched byproductive_id - Matches people to app users by email (case-insensitive)
- Resolves manager relationships (self-referencing FK)
- Updates matched users’
job_titleandpicturefrom Productive data - Upserts
user_external_idsfor theproductiveprovider - Removes people no longer active in Productive
Permissions
Section titled “Permissions”| Access Level | View | Update | Manage (Sync) |
|---|---|---|---|
| Executive | Yes | Yes | Yes |
| Head | Yes | No | No |
| Manager | Yes | No | No |
| Lead | Yes | No | No |
| Employee | Yes | No | No |
URL State
Section titled “URL State”| Parameter | Type | Description |
|---|---|---|
view | string | directory (default) or org-chart |
id | string | Person ID — when present, renders the full profile page |
tab | string | Profile tab: details, employment, documents, activity, notes, linkedin |
Data Model
Section titled “Data Model”people table
Section titled “people table”The people table is the source of truth for all employee/HR data. Employee fields (employment, contact, demographics, financial) are stored here rather than on users, which allows Xero sync for people who don’t have an app account. The users table is auth-only (id, email, name, picture, access_level, template IDs).
| Column | Type | Description |
|---|---|---|
| id | TEXT PK | UUID |
| productive_id | TEXT UNIQUE | Productive person ID |
| user_id | TEXT FK | Matched app user (nullable) |
| first_name | TEXT | First name |
| last_name | TEXT | Last name |
| TEXT | Email address | |
| title | TEXT | Job title from Productive |
| avatar_url | TEXT | Profile picture URL |
| squad | TEXT | Squad/team name |
| manager_id | TEXT FK | Self-referencing FK to manager’s people row |
| xero_employee_id | TEXT | Linked Xero employee GUID |
| linkedin_url | TEXT | LinkedIn profile URL |
| linkedin_activities_fetched_at | TEXT | Last activities fetch timestamp |
| alert_subscription_id | TEXT | Scrapin webhook subscription ID |
| alert_subscribed_at | TEXT | When alert subscription was created |
| start_date | TEXT | Employment start date |
| end_date | TEXT | Employment end date (nullable) |
| employment_status | TEXT | active (default) or terminated |
| employment_basis | TEXT | FULLTIME, PARTTIME, CASUAL, etc. |
| location | TEXT | Work location |
| gender | TEXT | Gender code from Xero |
| date_of_birth | TEXT | Date of birth |
| phone | TEXT | Work phone |
| mobile | TEXT | Mobile number |
| personal_email | TEXT | Personal email address |
| home_address | TEXT | Home address (JSON from Xero) |
| super_fund_name | TEXT | Superannuation fund name |
| super_fund_type | TEXT | REGULATED or SELFMANAGED |
| super_member_number | TEXT | Super fund member number |
| bank_account_name | TEXT | Bank account holder name |
| bank_bsb | TEXT | Bank BSB |
| bank_account_number | TEXT | Bank account number |
| synced_at | TEXT | Last sync timestamp |
person_notes table
Section titled “person_notes table”Confidential timestamped notes on person profiles with per-note visibility control.
| Column | Type | Description |
|---|---|---|
| id | TEXT PK | UUID |
| person_id | TEXT FK | References people(id) |
| author_id | TEXT FK | References users(id) — who wrote the note |
| content | TEXT | Rich text (HTML) content |
| visibility | TEXT | Comma-separated access levels (e.g. executive,head,manager) |
| created_at | TEXT | Creation timestamp |
| updated_at | TEXT | Last edit timestamp |
person_linkedin_activities table
Section titled “person_linkedin_activities table”Cached LinkedIn activities (posts, comments, reactions) fetched from Scrapin.
| Column | Type | Description |
|---|---|---|
| id | TEXT PK | UUID |
| person_id | TEXT FK | References people(id) |
| type | TEXT | post, reaction, or comment |
| activity_id | TEXT | Scrapin activity ID (dedup key) |
| content | TEXT | Post/comment text |
| reaction_type | TEXT | LIKE, LOVE, etc. (reactions only) |
| related_post_text | TEXT | Original post text (reactions/comments) |
| related_post_url | TEXT | Original post URL |
| reactions_count | INTEGER | Number of reactions |
| comments_count | INTEGER | Number of comments |
| activity_date | TEXT | When the activity occurred |
| activity_url | TEXT | Direct link to the LinkedIn activity |
| fetched_at | TEXT | When this was cached |
person_change_alerts table
Section titled “person_change_alerts table”Change alerts received via Scrapin webhooks when LinkedIn profile changes are detected.
| Column | Type | Description |
|---|---|---|
| id | TEXT PK | UUID |
| person_id | TEXT FK | References people(id) |
| subscription_id | TEXT | Scrapin subscription ID |
| linkedin_url | TEXT | LinkedIn URL at time of alert |
| previous_snapshot | TEXT | JSON of previous profile |
| current_snapshot | TEXT | JSON of current profile |
| changes_detected | TEXT | JSON array of {field, old, new} changes |
| received_at | TEXT | When the webhook was received |
Components
Section titled “Components”| File | Purpose |
|---|---|
src/routes/people.tsx | Route with ?view and ?id search params |
src/components/people/people-page.tsx | Main page with view toggle, search, sync button; renders profile page when id param present |
src/components/people/people-directory.tsx | Searchable/sortable table view |
src/components/people/person-profile.tsx | Full-page person profile with tabbed layout |
src/components/people/person-peek-sheet.tsx | Simplified peek drawer for org chart |
src/components/people/org-tree.tsx | Recursive tree visualization |
src/components/people/org-card.tsx | Individual person card in org chart |
src/components/people/linkedin-activities.tsx | LinkedIn activities section (posts, comments, reactions) |
src/components/people/change-alerts.tsx | Change alerts timeline with subscribe/unsubscribe |
src/hooks/use-people.ts | TanStack Query hooks |
worker/routes/people.ts | API endpoints |
worker/routes/webhooks.ts | Public webhook receiver for Scrapin change alerts |