mono/packages/ui/docs/contacts.md
2026-03-21 20:18:25 +01:00

217 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Contacts
User-managed address book — vCard-compatible contacts, groups, import/export, and flexible `meta` jsonb.
## Architecture Overview
```
┌──────────────────────────────────────┐
│ Frontend │
│ ContactsManager.tsx │
│ (MUI DataGrid, batch bar, dialogs) │
│ │
│ client-contacts.ts │
│ (fetch wrappers, bearer token) │
└──────────────┬───────────────────────┘
│ /api/contacts/*
┌──────────────────────────────────────┐
│ Server ContactsProduct │
│ products/contacts/index.ts │
│ products/contacts/routes.ts │
└──────────────┬───────────────────────┘
│ Supabase
┌──────────────────────────────────────┐
│ Tables │
│ contacts │
│ contact_groups │
│ contact_group_members │
└──────────────────────────────────────┘
```
## Database
### `contacts`
| Column | Type | Notes |
|--------|------|-------|
| `id` | uuid | PK, auto-generated |
| `owner_id` | uuid | FK → `auth.users`, not null |
| `name` | text | Full display name |
| `first_name` | text | — |
| `last_name` | text | — |
| `emails` | jsonb | Array of `{ email, label?, primary? }` objects |
| `phone` | text | Primary phone |
| `organization` | text | Company / org name |
| `title` | text | Job title |
| `address` | jsonb | Array of `{ street, city, state, postal_code, country, label? }` |
| `source` | text | Origin of contact (`cscart`, `import`, `manual`, …) |
| `language` | text | Preferred language tag (`en`, `de`, …) |
| `status` | text | `active` / `unsubscribed` / `bounced` / `blocked` |
| `notes` | text | Free-form notes |
| `tags` | text[] | Searchable tags |
| `log` | jsonb | Audit / event log array `[{ at, event, data }]` |
| `meta` | jsonb | Arbitrary extra fields (vCard extensions, etc.) |
| `created_at` | timestamptz | — |
| `updated_at` | timestamptz | Auto-updated via trigger |
**Indexes:** `owner_id`, `status`, `source`, `language`, `tags` (GIN), `emails` (GIN)
### `contact_groups`
| Column | Type | Notes |
|--------|------|-------|
| `id` | uuid | PK |
| `owner_id` | uuid | FK → `auth.users` |
| `name` | text | not null |
| `description` | text | — |
| `meta` | jsonb | e.g. color, icon |
| `created_at` | timestamptz | — |
| `updated_at` | timestamptz | — |
### `contact_group_members`
| Column | Type | Notes |
|--------|------|-------|
| `group_id` | uuid | FK → `contact_groups` |
| `contact_id` | uuid | FK → `contacts` |
| `added_at` | timestamptz | — |
| PK | composite | (`group_id`, `contact_id`) |
### RLS
- **Owners**: full CRUD on their own rows (`owner_id = auth.uid()`)
- **Admins** (`user_roles.role = 'admin'`): full access to all rows
## Server API Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/api/contacts` | Auth | List contacts. Query: `?group=<id>&q=<search>&status=<status>&limit=&offset=` |
| `POST` | `/api/contacts` | Auth | Create contact |
| `GET` | `/api/contacts/:id` | Auth | Get single contact |
| `PATCH` | `/api/contacts/:id` | Auth | Update contact (partial) |
| `DELETE` | `/api/contacts/:id` | Auth | Delete contact |
| `POST` | `/api/contacts/import` | Auth | Bulk import JSON array or vCard text (`?format=json\|vcard`) |
| `GET` | `/api/contacts/export` | Auth | Export all contacts. Query: `?format=json\|vcard&group=<id>` |
| `GET` | `/api/contact-groups` | Auth | List groups |
| `POST` | `/api/contact-groups` | Auth | Create group |
| `PATCH` | `/api/contact-groups/:id` | Auth | Update group |
| `DELETE` | `/api/contact-groups/:id` | Auth | Delete group |
| `GET` | `/api/contact-groups/members` | Auth | List all group memberships for the user's contacts `→ { contact_id, group_id }[]` |
| `POST` | `/api/contact-groups/:id/members` | Auth | Add contacts `{ contact_ids: string[] }` `→ { added: number }` |
| `DELETE` | `/api/contact-groups/:id/members/:contactId` | Auth | Remove contact from group |
> **Route priority:** static sub-paths (`/import`, `/export`, `/members`) are registered before parameterised `:id` routes to avoid conflicts.
## Import / Export Format
### JSON (default)
```json
[
{
"email": "jane@example.com",
"name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+1 555 0100",
"organization": "Acme",
"title": "Engineer",
"address": { "city": "Berlin", "country": "DE" },
"tags": ["customer", "newsletter"],
"meta": { "source": "cscart" }
}
]
```
### vCard (format=vcard)
Standard vCard 3.0 — one `BEGIN:VCARD … END:VCARD` block per contact.
Fields mapped: `FN`, `N`, `EMAIL`, `TEL`, `ORG`, `TITLE`, `ADR`, `NOTE`, `CATEGORIES`.
Extended fields stored in `meta` as `X-PM-*` (`X-PM-LANGUAGE`, `X-PM-SOURCE`, `X-PM-STATUS`).
## Frontend Client
`src/modules/contacts/client-contacts.ts` — all functions inject the Supabase bearer token automatically via `authHeaders()`. Requests are routed through a shared `apiFetch` helper that resolves `VITE_SERVER_IMAGE_API_URL`.
```ts
// Contacts CRUD
fetchContacts(options?) Contact[] // options: { group?, q?, status?, limit?, offset? }
getContact(id) Contact
createContact(data) Contact
updateContact(id, data) Contact
deleteContact(id) void
// Import / Export
importContacts(body, format?) { imported: number; skipped: number }
exportContacts(options?) string | Contact[] // options: { format?, group? }
// Groups CRUD
fetchContactGroups() ContactGroup[]
createContactGroup(data) ContactGroup
updateContactGroup(id, data) ContactGroup
deleteContactGroup(id) void
fetchGroupMembers() { contact_id: string; group_id: string }[]
addGroupMembers(groupId, contactIds) { added: number }
removeGroupMember(groupId, contactId) void
```
### Key Types
```ts
interface ContactEmail { email: string; label?: string; primary?: boolean }
interface ContactAddress { street?; city?; state?; postal_code?; country?; label? }
interface Contact { id; owner_id; name?; first_name?; last_name?; emails: ContactEmail[];
phone?; address: ContactAddress[]; source?; language?;
status?: 'active'|'unsubscribed'|'bounced'|'blocked';
organization?; title?; notes?; tags?; log?; meta?;
created_at?; updated_at? }
interface ContactGroup { id; owner_id; name; description?; meta?; created_at?; updated_at? }
```
## Frontend UI — `ContactsManager`
Full-featured management interface built with **MUI DataGrid** inside a shadcn/ui shell.
### Features
| Feature | Detail |
|---------|--------|
| **DataGrid** | Sortable, filterable columns (name, email, status, groups, tags, actions). Checkbox selection. |
| **URL state sync** | Filter, sort, column visibility and pagination models are persisted in URL search params via `gridUtils`. |
| **Toolbar filters** | Search (`q`), group dropdown, status dropdown — all reflected in URL and sent to the server. |
| **Contact dialog** | Create / edit form with email chips, tag chips, group toggles, and status select. |
| **Batch bar** | When rows are selected: set group, remove from all groups, set status, or delete. Uses `addGroupMembers`, `removeGroupMember`, `updateContact`, `deleteContact`. |
| **Import** | File picker accepts `.json` / `.vcf`, auto-detects format. |
| **Export** | Dropdown for JSON or vCard, respects active group filter. Downloads as file. |
| **Group management** | Dialog to create / delete groups. Inline in the toolbar. |
### URL Parameters
| Param | Source |
|-------|--------|
| `q` | Search input |
| `group` | Group filter dropdown |
| `status` | Status filter dropdown |
| `filter_*` | DataGrid column filters (via `gridUtils`) |
| `sort` | DataGrid sort model |
| `hidden` | DataGrid column visibility |
| `page` / `pageSize` | DataGrid pagination (defaults: 0 / 50) |
## Environment Variables
Inherits same Supabase env as the rest of the server — no additional variables required.
## Source Files
| File | Description |
|------|-------------|
| [contacts.md](contacts.md) | This document |
| [migration](../supabase/migrations/20260306120000_create_contacts.sql) | DB schema, RLS, indexes |
| [routes.ts](../server/src/products/contacts/routes.ts) | Zod-OpenAPI route definitions |
| [index.ts](../server/src/products/contacts/index.ts) | ContactsProduct handlers |
| [client-contacts.ts](../src/modules/contacts/client-contacts.ts) | Frontend fetch wrappers |
| [ContactsManager.tsx](../src/components/ContactsManager.tsx) | Main UI component (DataGrid, batch ops, dialogs) |