feat(contacts): Contacts & Org Charts (CO) — offices, affiliations, suggestions inbox#19
Merged
Merged
Conversation
…ons, suggestions inbox)
A standalone pre-launch capability: a top-level Contacts area plus a per-org
office hierarchy (the org chart), fed by a source-agnostic suggestions inbox.
Data model (migration 0008, additive + idempotent — NOT yet applied to the
shared Supabase; pending orchestrator coordination):
* contacts — people (org-agnostic; linked via affiliations)
* offices — first-class org-chart nodes (reports-to parent; can be VACANT)
* contact_affiliations — contact ↔ org (+ optional office) with tenure history + multi-org
* contact_suggestions — source-agnostic inbox (calendar attendees now, n8n web-scan later)
Hand-added the four tables to database.types.ts (no supabase CLI available) and
added domain types in @gracie/shared (types/contact.ts).
Backend:
* apps/web/lib/data/contacts.ts — contacts/offices/affiliations/suggestions + CSV/vCard export;
setOfficeHolder encapsulates the "end prior holder" invariant; buildOfficeTree.
* REST routes under /api/contacts/**, /api/clients/[id]/{offices,org-chart,contacts/export},
/api/contact-suggestions/** — read = contacts.view (all roles), writes = editor (contacts.edit).
* Worker: nightly contact-suggestions sweep scans meetings.external_attendees (P4.1) and
upserts pending suggestions, guessing the org by domain; skips existing contacts,
pending/dismissed suggestions, and free-email. Idempotent.
Web UI (/contacts, three sections):
* All contacts — searchable list + profile drawer (details, affiliation history, add/end
affiliation, delete, CSV + vCard download).
* Org charts — org picker → visual reports-to office tree with VACANT nodes and key-vacancy
flags; add/edit/delete office, fill/vacate holder; org-wide CSV export.
* Suggestions — pending inbox with Accept (→ contact + optional affiliation) / Dismiss.
Permissions: adds contacts.view (all) + contacts.edit (editor) to PERMISSIONS + PERMISSION_MATRIX.
Viewer is strictly read-only. Bot kill-switch untouched (stays OFF).
Green gate: pnpm -w typecheck && pnpm -w lint && pnpm --filter web build all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contacts & Org Charts (
CO) — a standalone pre-launch phaseA consultancy needs to know who holds which office at every client and partner (VA, etc.) and how to reach them. This adds a top-level Contacts area + a per-org office hierarchy (the org chart), plus a source-agnostic suggestions inbox fed from meeting attendees.
What's in it
is_key+ amber key-vacancy highlight).meetings.external_attendees(P4.1) and upserts pendingcontact_suggestions, guessing the org by domain. Surfaced in the Contacts area (not the calendar). Built source-agnostic so a future n8n web-scan feeds the same inbox (source='n8n_web').Data model — migration
0008_contacts_org_charts.sql(additive + idempotent)contacts,offices(reports-to parent,is_key, can be vacant),contact_affiliations(tenure + multi-org; a partial unique index enforces at most one current holder per office),contact_suggestions(source-agnostic queue with a pending-dedup index).Backend
apps/web/lib/data/contacts.ts— contacts / offices / affiliations / suggestions + CSV/vCard builders.setOfficeHolderencapsulates the end-prior-holder invariant (history preserved);buildOfficeTreebuilds the reports-to tree; office reparenting guards self-reference + cycles./api/contacts/**,/api/clients/[id]/{offices,org-chart,contacts/export},/api/contact-suggestions/**.contact-suggestions.processor.ts(nightly sweep) + queue, wired intoindex.ts. Idempotent — skips existing contacts, pending/dismissed suggestions, and free-email domains.Web UI —
/contacts(three sections)Permissions
Adds
contacts.view(all roles) +contacts.edit(editor tier) toPERMISSIONS+PERMISSION_MATRIX. Nav item + write routes gated accordingly. Viewer is strictly read-only. Bot kill-switch untouched (stays OFF).Green gate
pnpm -w typecheck✅ ·pnpm -w lint✅ ·pnpm --filter web build✅ (only the pre-existingunpdfimport-trace warning).Not in scope (per brief)
The n8n automation itself (only the queue hook), bulk CSV import, drag-and-drop org-chart reorg, migrating
clients.primary_contact*. The per-org client-detail Contacts sub-tab is left as a cheap fast-follow (components already exist).🤖 Generated with Claude Code