diff --git a/personio-headcount/references/data-schemas.md b/personio-headcount/references/data-schemas.md new file mode 100644 index 0000000..9563acb --- /dev/null +++ b/personio-headcount/references/data-schemas.md @@ -0,0 +1,134 @@ +# Data Schemas + +## Snapshot Structure + +```json +{ + "last_refreshed": "2026-03-04T19:00:00Z", + "total_records": 755, + "summary": { + "ACTIVE": 177, + "INACTIVE": 574, + "ONBOARDING": 1, + "LEAVE": 3 + }, + "employees": [ + { + "personio_id": "35653993", + "status": "ONBOARDING", + "team_id": "417186", + "department_id": "1291054", + "office_id": "501083", + "start_date": "2026-05-01", + "end_date": null + } + ] +} +``` + +## Team Mapping Structure + +Maps Personio team IDs to human-readable names: + +```json +{ + "total_teams": 65, + "teams": [ + { + "team_id": "4400959", + "team_name": "AI & Automation", + "active_headcount": 6, + "total_records": 7 + } + ] +} +``` + +## Department Mapping Structure + +Groups teams under departments with real names from `department-mapping.csv`: + +```json +{ + "total_departments": 19, + "departments": [ + { + "department_id": "1291054", + "department_name": "Finance & Business Navigation", + "active_headcount": 11, + "total_records": 45, + "team_ids": ["417186", "417184", "3442693", "3443053", "572110"], + "teams": ["Data", "Finance & Administration", "M&A", "Pricing", "Rev Ops"] + } + ] +} +``` + +## Office Mapping Structure + +```json +{ + "total_offices": 8, + "offices": [ + { + "office_id": "501083", + "active_headcount": 120, + "total_records": 500 + } + ] +} +``` + +## MCP Tool Reference + +**Tool:** `Call_Personio_Get_Employees_` (full ID: `mcp__n8n-personio__Call_Personio_Get_Employees_`) + +| Parameter | Type | Default | Description | +|-----------|--------|---------|--------------------------------------| +| limit | number | 50 | Records per page (use 50 for balance)| +| cursor | string | "" | Pagination cursor from previous page | + +Pass parameters as a JSON string in the `input` field: +- First page: `{"limit": 50}` +- Subsequent pages: `{"limit": 50, "cursor": ""}` + +## Employee Record Fields (from API) + +Each employee record from the MCP tool contains: +- `id` → store as `personio_id` +- `status` → ACTIVE, INACTIVE, ONBOARDING, or LEAVE +- `team_id` → Personio team ID (look up name in team_mapping.json) +- `department_id` → Personio department ID +- `office_id` → Personio office ID +- `start_date` → employment start +- `end_date` → contract end date (null if ongoing) +- `first_name`, `last_name`, `email` → **NEVER STORE THESE** — strip before saving + +## Incremental Refresh (Delta Update) + +1. Read existing snapshot +2. Fetch all pages from Personio (no "modified since" filter available) +3. Compare each employee by `personio_id`: + - ID exists → update the record (status, team_id, etc. may have changed) + - ID is new → add it + - ID in snapshot but not in fresh data → keep (historical) +4. Recalculate summary counts +5. Update `last_refreshed` timestamp +6. Rebuild team_mapping.json counts +7. Save all files + +## PDF Report Contents + +The PDF report has 6 pages: + +1. **Cover page** — KPI cards (active, onboarding, on leave, inactive) +2. **Headcount by Team** — all active teams ranked by headcount with percentages +3. **Headcount by Department** — departments with team listings +4. **New Joiners** — monthly breakdown since Jan 2025, with team attribution +5. **Departures** — monthly breakdown since Jan 2025, with team attribution +6. **Net Workforce Movement** — joiners vs leavers with cumulative net change + +### XML Escaping + +The report uses `xml_escape()` for special characters in names (e.g., "M&A" → "M&A"). +This is required because reportlab's Paragraph class uses an XML parser internally.