Files
resolutionflow/docs/plans/archive/2026-03-04-survey-invite-tracking-design.md
Michael Chihlas cbb4b25671
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 6m42s
CI / e2e (pull_request) Successful in 10m11s
CI / backend (pull_request) Successful in 10m43s
fix(ui): drop setState-in-effect in useAuthSessionExpiry
CI surfaced react-hooks/set-state-in-effect on the synchronous
setState(computeState(token)) inside the useEffect body. The earlier
shape mirrored token -> state via an effect, which is exactly the
"you might not need an effect" pattern React 19's eslint rule now
flags.

Switch to derived state: compute during render, use a useReducer
tick to force re-render on the 30s cadence (so relative timestamps
stay current even when token props don't change). Same observable
behavior, no cascading renders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:15:11 -04:00

77 lines
3.1 KiB
Markdown

# Survey Invite Tracking — Design
> **Date:** 2026-03-04
> **Status:** Approved
## Goal
Add invite tracking to the FlowPilot survey so Michael can create personalized links, optionally email them, and see who has/hasn't responded. Each invite token is single-use — one submission per token.
## Data Model
### New table: `survey_invites`
| Column | Type | Notes |
|--------|------|-------|
| `id` | UUID PK | |
| `token` | VARCHAR(32) UNIQUE | Random URL-safe token |
| `recipient_name` | VARCHAR(255) NOT NULL | Who it's for |
| `recipient_email` | VARCHAR(255) NULL | Only if emailing |
| `status` | VARCHAR(20) DEFAULT 'pending' | `pending` or `completed` |
| `email_sent` | BOOLEAN DEFAULT false | Whether Resend email was sent |
| `created_at` | TIMESTAMPTZ NOT NULL | |
| `completed_at` | TIMESTAMPTZ NULL | Set on submission |
### Modified table: `survey_responses`
Add `invite_id` UUID FK nullable → `survey_invites.id`. Responses from tokenless `/survey` have `invite_id = NULL`.
## API Endpoints
### Public (no auth)
- `GET /api/v1/survey/invite/{token}` — Returns invite status (`{ name, status }`). If `completed`, frontend shows "already submitted" screen. Returns 404 for invalid tokens.
- `POST /api/v1/survey/submit` — Modified: accepts optional `token` field. If token provided, validates it's `pending`, links the response, and marks invite as `completed`. Returns 409 if token already used.
### Admin (super_admin auth)
- `POST /api/v1/admin/survey-invites` — Create invite. Body: `{ recipient_name, recipient_email?, send_email? }`. Generates token, optionally sends email. Returns the invite with the full survey URL.
- `GET /api/v1/admin/survey-invites` — List all invites with status.
## Frontend
### Survey page changes (`/survey`)
- On load, reads `?t=<token>` from URL params
- If token present: calls `GET /survey/invite/{token}`
- If `completed` → show "already submitted" screen
- If `pending` → show survey, include token in submission payload
- If 404 → show survey without token (treat as open link)
- If no token: show survey as-is (open access)
### Admin page (`/admin/survey-invites`)
**Top section: Create Invite**
- Name input (required) + Email input (optional)
- "Generate Link" button → creates invite, shows URL with copy button
- "Send Email" button → creates invite with `send_email: true`, shows confirmation toast
- "Send Email" only enabled when email field is filled
**Bottom section: Invite Table**
- Columns: Name, Email, Status badge (pending amber / completed green), Sent (email icon or dash), Created date, Completed date
- Sorted by created_at descending
## Email Template
Uses existing `EmailService` + Resend pattern. Dark-themed email matching Slate & Ice aesthetic:
- Subject: "FlowPilot Survey — Your expertise matters"
- Body: Brief intro, CTA button linking to `/survey?t=<token>`, ~5 minutes note
- From: existing `FROM_EMAIL` config
## Constraints
- No token expiration
- No reminder/resend (keep it simple)
- Tokenless survey still works for open sharing
- One submission per invite token (enforced backend + frontend)