From 6239eed6e6a4d3ca96251e95f32c7299a3387089 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 11 Jun 2026 05:56:55 +0200 Subject: [PATCH] docs(research): complete v2.0 stack, features, architecture, and pitfalls analysis Synthesized research outputs from 4 parallel researcher agents: - STACK.md: 7 new npm packages (dnd-kit, TanStack Table, RHF, BullMQ), React 19 compat verified - FEATURES_v2.0.md: Feature landscape (table stakes, differentiators, anti-features), MVP breakdown - ARCHITECTURE.md: Schema additions (services, offer_phases, leads, quotes), 5-phase build order - PITFALLS.md: Critical/moderate/minor pitfalls + prevention strategies Ready for roadmap planning and phase-by-phase execution. Co-Authored-By: Claude Haiku 4.5 --- .planning/research/ARCHITECTURE.md | 973 ++++++++++++++++++++++++---- .planning/research/FEATURES_v2.0.md | 412 ++++++++++++ .planning/research/STACK.md | 427 +++++++++--- 3 files changed, 1584 insertions(+), 228 deletions(-) create mode 100644 .planning/research/FEATURES_v2.0.md diff --git a/.planning/research/ARCHITECTURE.md b/.planning/research/ARCHITECTURE.md index d5d84b1..d30d59c 100644 --- a/.planning/research/ARCHITECTURE.md +++ b/.planning/research/ARCHITECTURE.md @@ -1,191 +1,876 @@ -# Architecture — ClientHub Freelancer Client Portal +# Architecture Patterns: v2.0 Business Operations Suite Integration -**Project:** ClientHub (welcomeclient.iamcavalli.net) -**Researched:** 2026-05-09 +**Project:** ClientHub v2.0 +**Researched:** 2026-06-10 +**Scope:** Unified service catalog, offer phases, public quote pages, CRM pipeline integration with existing v1.0 architecture **Confidence:** HIGH --- +## Executive Summary + +ClientHub v2.0 extends v1.0 from a client dashboard portal into a full business operations suite by unifying the fragmented services taxonomy, introducing offer templates with phases, enabling public quote generation, and adding a lead CRM that converts wins into projects. The integration preserves all existing v1.0 guarantees (token-based client access, immutable approvals, quote item secrecy) while adding: + +- **One canonical services table** replacing the parallel `service_catalog` (for quotes) + `offer_services` (for marketing) dualism +- **Offer phases system** that templates project structure and enables "copy-on-win" mechanics +- **Public quote routes** at `/quote/[token]` mirroring client `/client/[token]` token-gating pattern +- **Lead CRM** that auto-provisions clients, projects, and payment schedules on quote acceptance + +The architecture keeps everything in one Next.js app + one Postgres database. No service separation. Middleware uses the same token-validation pattern. All data flows are additive-first to respect Data Safety constraints (never delete/truncate clients, projects, payments, phases). + +--- + +## System Topology: v1.0 → v2.0 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Next.js 16 App Router (Single Container on Coolify) │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ ┌─ /admin/* (Session → Auth.js) │ +│ │ ├─ /admin/clients [v1.0 ✓] │ +│ │ ├─ /admin/projects [v1.0 ✓] │ +│ │ ├─ /admin/catalog [NEW — unifies services] │ +│ │ ├─ /admin/offers [Phase 5 → extended] │ +│ │ ├─ /admin/quotes [NEW — quote builder] │ +│ │ ├─ /admin/leads [NEW — CRM pipeline] │ +│ │ └─ /admin/dashboard [Phase 6 ✓] │ +│ │ │ +│ ├─ /client/[token]/* (Token → internal validation) │ +│ │ ├─ /client/[token]/dashboard [v1.0 ✓] │ +│ │ ├─ /client/[token]/projects [v1.0 ✓] │ +│ │ └─ /client/[token]/payments [v1.0 ✓] │ +│ │ │ +│ ├─ /quote/[token]/* (Token → public quote page) │ +│ │ ├─ /quote/[token] [NEW — multistep form] │ +│ │ └─ /quote/[token]/pdf [NEW — downloadable PDF] │ +│ │ │ +│ └─ /api/* │ +│ ├─ /api/client/* (client-facing APIs) │ +│ ├─ /api/admin/* (admin-only actions) │ +│ └─ /api/internal/* (validation endpoints) │ +│ │ +└──────────────────────┬──────────────────────────────────┘ + ↓ + Postgres (Neon) + Single schema +``` + +--- + ## Component Boundaries -Single Next.js application on Vercel. No separate backend. All server logic lives in Route Handlers (`/api/**`). One Postgres database (Neon serverless) accessed via Drizzle ORM. Admin auth via env-var secret + cookie. Client access via UUID token in URL — no auth library needed for clients. - -| Component | Responsibility | Communicates With | -|-----------|---------------|-------------------| -| Client Portal `/c/[token]` | Read-only view: status, phases, tasks, deliverables, payments, documents | API Routes (GET only) | -| Admin Dashboard `/admin` | List all clients with status summary | API Routes (full CRUD) | -| Admin Client Workspace `/admin/clients/[id]` | Edit phases, tasks, deliverables, payments, documents | API Routes (full CRUD) | -| Service Catalog Manager `/admin/catalog` | CRUD on service items + unit prices | API Routes (catalog entity) | -| Quote Builder `/admin/clients/[id]/quote` | Compose quote from catalog items, write `accepted_total` to client row | Catalog + API Routes | -| Comments System | Client posts on task/deliverable; admin replies | API Route POST | -| Approval Flow | Client PATCHes a deliverable to `approved` | API Route, validates token ownership | -| API Routes `/api/**` | Validate token or admin session; query/mutate DB; return JSON | Postgres only | -| Database | Single source of truth | API Routes only — never queried from browser | +| Component | Responsibility | Scope | Communicates With | +|-----------|---------------|-------|-------------------| +| **Unified Services** | Catalog of work units with pricing | `services` table (replaces `service_catalog` + `offer_services`) | offer_phases, quote_builder | +| **Offer System** | Marketing templates: macro/micro + phases with services | `offer_macros`, `offer_micros`, `offer_phases`, `offer_phase_services` | projects (on win), quote_builder, CRM | +| **Quote Builder** | Admin creates custom quotes from client + offer tiers + per-item pricing overrides | `quotes`, `quote_items` (extended with offer context) | public quote page, CRM (on acceptance) | +| **Public Quote Page** | Multistep form: lead selects offer tier, reviews total, accepts via link | `/quote/[token]/*` routes | quote records, CRM (on submission) | +| **CRM Pipeline** | Lead tracking, follow-up reminders, quote prerequisite, win→auto-provision | `leads`, `lead_activities`, `lead_contacts`, `quotes` (status) | projects, clients, payments (on win) | +| **Client Dashboard** | Read-only: project phases, tasks, deliverables, progress (v1.0 unchanged) | `/client/[token]/*`, `projects`, `phases`, `deliverables` | offers (view-only) | +| **Admin Dashboard** | KPI overview, activity feed, sidebar navigation | `/admin/dashboard` | all tables (read-only aggregation) | --- -## Data Flow +## Data Model Additions & Modifications -**Client reading their dashboard:** -``` -Browser → GET /c/[token] -→ Next.js server component -→ DB: clients WHERE token = [token] → 404 if missing -→ JOIN: project + phases + tasks + deliverables + payments + documents -→ Omit: quote_items, service prices -→ Render read-only portal +### NEW: `services` Table (Unified Catalog) + +Replaces the parallel `service_catalog` (used by quotes) + `offer_services` (used by offers) with a single source of truth. + +```typescript +export const services = pgTable("services", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), // "Brand Strategy" | "Content Calendar" + description: text("description"), + unit_price: numeric("unit_price", { precision: 10, scale: 2 }).notNull(), + category: text("category"), // "strategy" | "content" | "delivery" + active: boolean("active").notNull().default(true), + // Metadata for migration tracking (enables safe rollback) + migrated_from: text("migrated_from"), // "service_catalog" | "offer_services" | null + migrated_id: text("migrated_id"), // original ID from old table + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); ``` -**Client posting a comment:** -``` -Browser → POST /api/comments { token, entity_type, entity_id, body } -→ Validate token → write comment { author: 'client' } -→ 201 → re-fetch thread +**Why:** Eliminates dual catalog maintenance. One price source of truth. The `migrated_from` + `migrated_id` fields allow safe rollback if consolidation needs reversal during Phase 7 (Catalog Migration). + +### MODIFIED: `quote_items` Table + +Change references to unified services table; add offer context fields. + +```typescript +export const quote_items = pgTable("quote_items", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + quote_id: text("quote_id") // NEW: associate with quote header + .notNull() + .references(() => quotes.id, { onDelete: "cascade" }), + service_id: text("service_id") // CHANGED: service_catalog.id → services.id + .notNull() + .references(() => services.id, { onDelete: "restrict" }), + offer_micro_id: text("offer_micro_id") // NEW: links to source offer tier (nullable) + .references(() => offer_micros.id, { onDelete: "set null" }), + offer_phase_id: text("offer_phase_id") // NEW: which offer phase contains this (nullable) + .references(() => offer_phases.id, { onDelete: "set null" }), + quantity: numeric("quantity", { precision: 10, scale: 2 }).notNull(), + unit_price: numeric("unit_price", { precision: 10, scale: 2 }).notNull(), // snapshot at time of quote + subtotal: numeric("subtotal", { precision: 10, scale: 2 }).notNull(), + custom_label: text("custom_label"), // free-form overrides +}); ``` -**Client approving a deliverable:** -``` -Browser → PATCH /api/deliverables/[id]/approve { token } -→ Validate token owns deliverable → set status='approved', approved_at=now() -→ Return updated deliverable +**Why:** Separates quote as a document header from line items. Links quote to offer context for traceability. Still never exposed via client API (security maintained). + +### NEW: `offer_phases` Table (Offer Template Structure) + +Templates the hierarchical breakdown of an offer into phases (e.g., Discovery → Strategy → Execution). + +```typescript +export const offer_phases = pgTable("offer_phases", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + micro_id: text("micro_id") + .notNull() + .references(() => offer_micros.id, { onDelete: "cascade" }), + title: text("title").notNull(), // "Discovery" | "Strategy" | "Execution" + description: text("description"), + sort_order: integer("sort_order").notNull().default(0), + duration_weeks: integer("duration_weeks"), // optional: how long this phase lasts + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); ``` -**Admin editing:** -``` -Browser (admin) → PATCH /api/admin/tasks/[id] + admin cookie -→ Validate session → update row → return updated task +**Why:** Offer micros currently lack phase structure. This lets us template the project's `phases` table. On CRM win, we copy `offer_phases` → `project.phases` (template → instance). + +### NEW: `offer_phase_services` Junction + +Assigns unified services to offer phases (replaces `offer_micro_services` + `offer_services` path). + +```typescript +export const offer_phase_services = pgTable( + "offer_phase_services", + { + phase_id: text("phase_id") + .notNull() + .references(() => offer_phases.id, { onDelete: "cascade" }), + service_id: text("service_id") + .notNull() + .references(() => services.id, { onDelete: "cascade" }), + }, + (t) => ({ + pk: primaryKey({ columns: [t.phase_id, t.service_id] }), + }) +); ``` -**Quote building:** +**Why:** Offer phases need services assigned. This replaces the old `offer_micro_services` → `offer_services` path with `offer_phase_services` → unified `services`. + +### NEW: `quotes` Table (Quote Record Header) + +Separate quote as a document from its line items, enabling tracking of viewing/acceptance/PDF generation. + +```typescript +export const quotes = pgTable("quotes", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + lead_id: text("lead_id") // nullable for standalone quotes to clients + .references(() => leads.id, { onDelete: "cascade" }), + client_id: text("client_id") // nullable for leads + .references(() => clients.id, { onDelete: "cascade" }), + token: text("token") // public page access token + .notNull() + .unique() + .$defaultFn(() => nanoid()), + pdf_url: text("pdf_url"), // generated PDF link (external or internal) + total_amount: numeric("total_amount", { precision: 10, scale: 2 }).notNull(), + status: text("status") // draft | sent | viewed | accepted | rejected + .notNull() + .default("draft"), + accepted_at: timestamp("accepted_at", { withTimezone: true }), + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); ``` -Admin UI selects services → computes line items -→ POST /api/admin/clients/[id]/quote { line_items[], accepted_total } -→ Write quote_items rows + write clients.accepted_total (denormalized) -→ Client portal reads clients.accepted_total — never touches quote_items + +**Why:** Separates quote as a document/record with its own lifecycle from quote_items (line items). Allows tracking viewing, acceptance, PDF generation, and token-based public access. + +### NEW: `leads` Table (CRM Pipeline) + +Central lead record tracking from first contact through win. + +```typescript +export const leads = pgTable("leads", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + email: text("email").notNull(), + phone: text("phone"), + company: text("company"), + status: text("status") // new | contacted | qualified | quote_sent | won | lost + .notNull() + .default("new"), + // When won, these auto-populate (immutable once set) + client_id: text("client_id") + .references(() => clients.id, { onDelete: "set null" }), + project_id: text("project_id") + .references(() => projects.id, { onDelete: "set null" }), + // The quote they accepted + won_quote_id: text("won_quote_id") + .references(() => quotes.id, { onDelete: "set null" }), + // Tracking + last_contacted_at: timestamp("last_contacted_at", { withTimezone: true }), + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + won_at: timestamp("won_at", { withTimezone: true }), +}); +``` + +**Why:** Central lead record. Tracks lifecycle and links to the quote that closed the deal. Client/project auto-populate on win (immutable once set — another audit trail). + +### NEW: `lead_activities` Table + +Activity log for each lead, enabling follow-up tracking and reminders. + +```typescript +export const lead_activities = pgTable("lead_activities", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + lead_id: text("lead_id") + .notNull() + .references(() => leads.id, { onDelete: "cascade" }), + type: text("type").notNull(), // "call" | "email" | "meeting" | "quote_sent" | "note" + summary: text("summary").notNull(), + notes: text("notes"), + scheduled_for: timestamp("scheduled_for", { withTimezone: true }), // for reminders + completed_at: timestamp("completed_at", { withTimezone: true }), + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); +``` + +**Why:** Activity log for each lead. Enables "who to follow up with today" dashboard widget. Scheduled activities for reminders. + +### NEW: `lead_contacts` Table + +Tracks multiple stakeholders per lead. + +```typescript +export const lead_contacts = pgTable("lead_contacts", { + id: text("id") + .primaryKey() + .$defaultFn(() => nanoid()), + lead_id: text("lead_id") + .notNull() + .references(() => leads.id, { onDelete: "cascade" }), + name: text("name").notNull(), + role: text("role"), // "Founder" | "Marketing Manager" + email: text("email"), + phone: text("phone"), + created_at: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); +``` + +**Why:** Some leads have multiple stakeholders. This tracks key decision-makers per lead. + +### MODIFIED: `projects` & `phases` (Audit Trail) + +Add optional fields to track template origin for audit and rebuild scenarios. + +```typescript +// projects — new fields +export const projects = pgTable("projects", { + // ... existing fields ... + offer_id: text("offer_id") // which offer template (if any) + .references(() => offer_micros.id, { onDelete: "set null" }), + created_from_lead_id: text("created_from_lead_id"), // which lead won → created this +}); + +// phases — new fields +export const phases = pgTable("phases", { + // ... existing fields ... + offer_phase_id: text("offer_phase_id"), // original offer template phase (immutable reference) +}); +``` + +**Why:** Audit trail + templating linkage. Allows rebuilding phase/task structures from offer if needed, and traces project origin to CRM win. + +--- + +## Data Flow: New Features + +### Flow 1: Catalog Unification → Offer Build → Quote + +``` +Admin → /admin/catalog + Create/Edit unified `services` + ↓ + (Catalog migration in Phase 7: service_catalog + offer_services → services) + ↓ +Admin → /admin/offers/[id]/phases + Select offer macro (e.g., "Signature") + Select offer micro (e.g., "Signature A") + Define phases within micro (e.g., "Discovery → Strategy → Execution") + Assign unified `services` to each phase (drag-drop UI) + ↓ +Admin → /admin/quotes/new or /admin/clients/[id]/quotes + Select offer tier (micro_id) + System pre-populates: offer_phases → quote_items + Override per-item prices if needed + Generate quote PDF + Send to lead/client via email +``` + +### Flow 2: Public Quote → Acceptance → CRM Win + +``` +Lead receives /quote/[token] link (via email, Slack, etc.) + ↓ + Opens multistep form: + Step 1: Review offer structure (phases + services) + Step 2: Review total amount + Step 3: Input approval name + email + ↓ + Submit → quote.status = "accepted" + lead.status = "won" + ↓ + Trigger auto-provisioning SERVER ACTION (winLead): + └─→ Create `clients` (from lead.name + company + auto-slug) + └─→ Create `projects` (from offer title + client) + └─→ Copy `offer_phases` → `project.phases` (template → instance) + └─→ Create `tasks` (generated from phase/service structure) + └─→ Create `payments` (1-4 installments, configured per offer or case) + └─→ Update lead: client_id, project_id, won_quote_id, won_at + ↓ + Client auto-receives /client/[slug or token] link + (with project visibility, onboarding tasks, payment schedule) +``` + +### Flow 3: Client Dashboard (v1.0 Unchanged) + +``` +Client visits /client/[token]/dashboard + ↓ + Middleware validates token → routes to ClientDashboard component + ↓ + Display: projects → phases → tasks → deliverables + Also: view active offers if assigned (via project_offers) + Also: payment schedule + status + ↓ + Client approves deliverables (approved_at immutable) + Client leaves comments + ↓ + NEVER: quote item details, individual service prices +``` + +### Flow 4: Admin Dashboard (Extended) + +``` +Admin visits /admin/dashboard + ↓ + KPI Section: Total active clients, active projects, revenue forecast + ↓ + Activity Feed: + ├─ Recent leads (new → contacted → quote_sent → won) + ├─ Follow-up reminders: lead_activities.scheduled_for = today & !completed_at + ├─ Quotes pending approval (status = "sent") + ├─ Recent project updates (deliverables approved) + └─ Upcoming payments due ``` --- -## Data Model +## Middleware & Route Gating -``` -clients - id UUID PK - name TEXT - brand_name TEXT - brief TEXT - token UUID UNIQUE ← the secret link key (separate from PK!) - accepted_total NUMERIC ← denormalized; only price client ever sees - created_at TIMESTAMPTZ +### Existing Patterns (v1.0 — Preserved) -phases - id UUID PK - client_id UUID → clients.id - title TEXT - sort_order INT - status TEXT (upcoming | active | done) +```typescript +// src/proxy.ts — Next.js 16 edge middleware -tasks - id UUID PK - phase_id UUID → phases.id - title TEXT - description TEXT - status TEXT (todo | in_progress | done) - sort_order INT +// ✓ /admin/* → Auth.js session check +if (pathname.startsWith("/admin")) { + const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); + if (!token) { + return NextResponse.redirect(new URL("/admin/login", request.url)); + } + return NextResponse.next(); +} -deliverables - id UUID PK - task_id UUID → tasks.id - title TEXT - url TEXT ← external link (Google Drive, PDF, etc.) - status TEXT (pending | submitted | approved) - approved_at TIMESTAMPTZ ← immutable audit trail - -comments - id UUID PK - entity_type TEXT (task | deliverable) - entity_id UUID - author TEXT (client | admin) - body TEXT - created_at TIMESTAMPTZ - -payments - id UUID PK - client_id UUID → clients.id - label TEXT ("Acconto 50%" / "Saldo 50%") - amount NUMERIC - status TEXT (da_saldare | inviata | saldato) - paid_at TIMESTAMPTZ - -documents - id UUID PK - client_id UUID → clients.id - label TEXT - url TEXT - created_at TIMESTAMPTZ - -service_catalog - id UUID PK - name TEXT - description TEXT - unit_price NUMERIC - active BOOLEAN - -quote_items - id UUID PK - client_id UUID → clients.id - service_id UUID → service_catalog.id - quantity NUMERIC - unit_price NUMERIC ← snapshot at time of quote - subtotal NUMERIC - -- NEVER exposed via client API +// ✓ /client/[slug or token]/* → internal token/slug validation +if (pathname.startsWith("/client/")) { + const slugOrToken = extractFromPath(); + const validation = await validateSlugOrToken(slugOrToken, INTERNAL_SECRET); + if (!validation.ok) return NextResponse.rewrite(new URL("/not-found", request.url)); + return NextResponse.next(); +} ``` -**Key design decisions:** -- `clients.token` is the only secret. Rotation = single UPDATE. No session store needed. -- `clients.accepted_total` is deliberately denormalized so client API never touches `quote_items`. -- Approval `approved_at` stored as immutable audit trail — disputes resolved by timestamp. -- `comments` use `entity_type + entity_id` polymorphic pair — correct at this scale. -- `payments` always two rows per client (created when quote is finalized). +### New Pattern: Public Quote Routes + +```typescript +// Add to src/proxy.ts (after /client check) + +// NEW /quote/[token]/* → public quote validation (no session) +if (pathname.startsWith("/quote/")) { + const tokenMatch = pathname.match(/^\/quote\/([a-zA-Z0-9_-]+)/); + if (!tokenMatch) return NextResponse.rewrite(new URL("/not-found", request.url)); + + const token = tokenMatch[1]; + + try { + // Validate quote exists and is current (via internal API) + const port = process.env.PORT ?? "3000"; + const res = await fetch( + `http://localhost:${port}/api/internal/validate-quote-token?token=${encodeURIComponent(token)}`, + { headers: { "x-internal-secret": process.env.INTERNAL_SECRET ?? "" } } + ); + + if (!res.ok) { + return NextResponse.rewrite(new URL("/not-found", request.url)); + } + + return NextResponse.next(); + } catch { + return NextResponse.rewrite(new URL("/not-found", request.url)); + } +} +``` + +**Why:** Public quotes are gated by token (like client links) but don't require a session. Anyone with the link can view/accept. Middleware doesn't authenticate; it just validates the quote exists and is not expired. --- -## Suggested Build Order +## New Routes & Components -``` -1. DB schema + migrations - └─ everything depends on this +### Admin Routes (New) -2. API: token lookup + project read (GET only) - └─ unblocks client portal +| Route | Component | Responsibility | Gated By | Phase | +|-------|-----------|-----------------|----------|-------| +| `/admin/catalog` | ServiceCatalogPage | CRUD unified services | Auth.js | 7 | +| `/admin/offers/[id]/phases` | OfferPhaseBuilder | Manage phases + drag-drop services into phases | Auth.js | 8 | +| `/admin/quotes` | QuoteListPage | List all quotes, filter by status, view PDF | Auth.js | 9 | +| `/admin/quotes/new` | QuoteBuilderPage | Create quote (select offer, client, override pricing) | Auth.js | 9 | +| `/admin/leads` | LeadListPage | CRM pipeline (new → contacted → quote_sent → won) | Auth.js | 10 | +| `/admin/leads/[id]` | LeadDetailPage | View lead, manage contacts, log activities, send quote | Auth.js | 10 | -3. Client portal UI /c/[token] - └─ the core deliverable; clients need this first +### Public Routes (New) -4. Admin auth middleware (env-var secret, cookie check) - └─ gate before admin routes go live +| Route | Component | Responsibility | Gated By | Phase | +|-------|-----------|-----------------|----------|-------| +| `/quote/[token]` | QuotePublicPage | Multistep form: review offer → accept → sign | Quote token | 9 | +| `/quote/[token]/pdf` | QuotePDFRoute | Download PDF | Quote token | 9 | +| `/api/public/quote/accept` | AcceptQuoteHandler | Form submission (POST) — triggers winLead | Quote token | 11 | -5. Admin: client list + client workspace CRUD - └─ phases, tasks, status, documents, payments +### Server Actions (New) -6. Comments system + deliverable approval - └─ depends on both client portal and admin workspace +All mutations use Server Actions (v1.0 pattern preserved): -7. Service catalog CRUD ← can run parallel with step 5 - └─ independent of client-facing features +```typescript +// src/app/admin/catalog/actions.ts — Service CRUD +export async function createService(data: NewService) { + await requireAdmin(); + return db.insert(services).values(data); +} -8. Quote builder - └─ depends on catalog + client entity +export async function updateService(id: string, data: Partial) { + await requireAdmin(); + return db.update(services).set(data).where(eq(services.id, id)); +} -9. Claude onboarding flow (v2) - └─ depends on all CRUD APIs being complete +// src/app/admin/leads/actions.ts — Lead & CRM +export async function createLead(data: NewLead) { + await requireAdmin(); + return db.insert(leads).values(data); +} + +export async function logLeadActivity(leadId: string, activity: NewLeadActivity) { + await requireAdmin(); + const [updated] = await db + .update(leads) + .set({ last_contacted_at: new Date() }) + .where(eq(leads.id, leadId)) + .returning(); + + await db.insert(lead_activities).values({ lead_id: leadId, ...activity }); + return updated; +} + +export async function sendQuoteToLead(leadId: string, quoteId: string) { + await requireAdmin(); + await db.update(quotes).set({ status: "sent" }).where(eq(quotes.id, quoteId)); + await logLeadActivity(leadId, { type: "quote_sent", summary: `Quote sent`, ... }); + // TODO: Email integration (future phase) +} + +export async function winLead(leadId: string, quoteId: string) { + await requireAdmin(); + + // Validate quote status + const quote = await db.query.quotes.findFirst({ where: eq(quotes.id, quoteId) }); + if (!quote || quote.status !== "accepted") throw new Error("Quote not accepted"); + + // Create client + const [newClient] = await db.insert(clients).values({ + name: lead.name, + brand_name: lead.company || lead.name, + brief: "", // optional + token: nanoid(), + slug: generateSlug(lead.name), + }).returning(); + + // Create project + const [newProject] = await db.insert(projects).values({ + client_id: newClient.id, + name: `Project for ${lead.company || lead.name}`, + offer_id: quote.offer_micro_id, // track origin + created_from_lead_id: leadId, + }).returning(); + + // Copy offer_phases → project.phases + const offerPhases = await getOfferPhasesByMicroId(quote.offer_micro_id); + for (const offerPhase of offerPhases) { + const [newPhase] = await db.insert(phases).values({ + project_id: newProject.id, + title: offerPhase.title, + description: offerPhase.description, + sort_order: offerPhase.sort_order, + offer_phase_id: offerPhase.id, // audit trail + status: "upcoming", + }).returning(); + + // TODO: Optionally create tasks from phase services + } + + // Create payments (1-4 installments) + const installments = [ + { label: "Acconto 50%", amount: quote.total_amount * 0.5 }, + { label: "Saldo 50%", amount: quote.total_amount * 0.5 }, + ]; + for (const payment of installments) { + await db.insert(payments).values({ + project_id: newProject.id, + ...payment, + status: "da_saldare", + }); + } + + // Update lead + const [updatedLead] = await db.update(leads).set({ + client_id: newClient.id, + project_id: newProject.id, + won_quote_id: quoteId, + status: "won", + won_at: new Date(), + }).where(eq(leads.id, leadId)).returning(); + + return { lead: updatedLead, client: newClient, project: newProject }; +} ``` --- -## Roadmap Implications +## Integration Points & Modified Components + +### Quote Items (Modified Behavior) + +**v1.0:** `quote_items` directly references `project_id` and `service_catalog.id`. + +**v2.0:** `quote_items` now belongs to a `quote` (header) and optionally to offer context via `offer_micro_id` + `offer_phase_id`. + +**Impact:** +- Quote builder pre-populates items from offer phases +- Client API still never exposes quote_items (security unchanged) +- Quote PDF generation uses quote_items + quote header + +### Offer System (Extended with Phases) + +**v1.0:** Offer micros → flat `offer_micro_services` → `offer_services`. No phase structure. + +**v2.0:** Offer micros → `offer_phases` → `offer_phase_services` → unified `services`. + +**Impact:** +- Phase 5 offer builder UI needs redesign: from flat "assign services to micro" → hierarchical "manage phases > assign services to phase" +- Old `offer_micro_services` deprecated but kept for rollback safety +- `offer_micro_services` → `offer_phase_services` data migration in Phase 7 + +### Project Phases (Copy-on-Win Mechanic) + +**v1.0:** Phases created manually per project by admin. + +**v2.0:** Phases can be copied from offer template on CRM win. + +**Impact:** +- `project.phases` now optionally linked to `offer_phases` via `offer_phase_id` +- Admin can still create/edit phases manually (template copy doesn't lock them) +- Ensures consistency while allowing customization + +### Client API (Unchanged Security) + +**Constraint preserved:** `/client/[token]` endpoints never expose `quote_items`, `lead_id`, `lead` status, or pricing details. + +**Nothing new exposed.** + +--- + +## Suggested Build Order (Phases 7–11) + +### Phase 7: Catalog Migration (Foundation) +**Duration:** 3–4 days +**Why first:** All downstream features depend on unified `services` table. + +**Tasks:** +1. Create new `services` table with `migrated_from` + `migrated_id` audit fields +2. Copy `service_catalog` → `services` (with `migrated_from = "service_catalog"`) +3. Copy `offer_services` → `services` (with `migrated_from = "offer_services"`) + - Handle duplicates: consolidate by name, keep both if distinct +4. Update `quote_items.service_id` FK from `service_catalog.id` → `services.id` +5. Update `offer_phase_services` (new junction) to reference `services.id` instead of old `offer_services.id` +6. Keep old tables for rollback safety (schema comments mark as "deprecated") +7. Update Zod validators for all new/updated types +8. Test: quote_items still load correctly; offer system reads unified services + +**Outputs:** +- `services` table + relations +- Data migration script + rollback plan +- Zod types + +**Not done yet:** Old tables still live; soft-delete comes in Phase 12+ cleanup. + +--- + +### Phase 8: Offer Phases & Quote Headers (Templating) +**Duration:** 2–3 days +**Why:** Sets up data model for offer-to-quote-to-project copy mechanics. + +**Tasks:** +1. Create `offer_phases` table (belongs to `offer_micros`) +2. Create `offer_phase_services` junction (new; old `offer_micro_services` deprecated) +3. Create `quotes` table (header, separate from `quote_items`) +4. Modify `quote_items` schema: add `quote_id` (FK), `offer_micro_id` (nullable), `offer_phase_id` (nullable) +5. Modify `projects` schema: add `offer_id`, `created_from_lead_id` +6. Modify `phases` schema: add `offer_phase_id` for audit trail +7. Create Drizzle migration file (additive: no drops) +8. Update all Zod validators + +**Outputs:** +- Full schema for offer phases + quote templating +- Migration script (additive-first) +- Zod validators + +**Not done yet:** No UI, no data population; schema only. + +--- + +### Phase 9: Quote Builder & Public Quote Routes +**Duration:** 4–5 days +**Why:** Quote generation + public pages are critical path for both direct quotes + CRM flow. + +**Tasks:** +1. Refactor existing quote builder (`/admin/clients/[id]/quote-actions.ts`): + - Create `quote` header + - Build `quote_items` from offer phases + override pricing + - Generate PDF (HTML → image/PDF) +2. Create `/admin/quotes` list page (filter: draft, sent, accepted, rejected) +3. Create `/admin/quotes/new` builder page (select client or lead, select offer) +4. Add middleware validation for `/quote/[token]` routes +5. Create `/quote/[token]` multistep page (review → accept) +6. Create `/quote/[token]/pdf` page (download/view) +7. Implement `/api/public/quote/accept` endpoint (POST to accept quote + trigger winLead on demand) +8. Server actions: `createQuote`, `updateQuote`, `acceptQuote`, `generatePDF` + +**Outputs:** +- Refactored quote builder +- Quote CRUD endpoints +- Public quote routes + validation +- PDF generation utility +- Integration test: build quote → send to lead → lead accepts via public link + +**Not done yet:** CRM integration beyond quote link; that's Phase 11. + +--- + +### Phase 10: CRM Pipeline (Leads & Activities) +**Duration:** 3–4 days +**Why:** Leads are entry point to win-→-provision flow. + +**Tasks:** +1. Create `leads`, `lead_activities`, `lead_contacts` tables +2. Create `/admin/leads` list page (sortable by status, last_contacted_at) +3. Create `/admin/leads/[id]` detail page: + - Contacts list + add new + - Activity log (calls, emails, notes, quote_sent) + - Activity form: date, type, notes, optional reminder + - "Send Quote" button → creates quote + updates lead.status = "quote_sent" + - Timeline view +4. Implement "Send Quote to Lead" flow (quote builder modal + action) +5. Admin dashboard widget: "Follow-ups due today" +6. Server actions: `createLead`, `logActivity`, `updateLeadStatus`, `sendQuoteToLead` + +**Outputs:** +- CRM data tables + migrations +- Lead detail UI + activity log +- Admin dashboard follow-up widget +- "Send Quote" integration + +**Not done yet:** Winning leads (that's Phase 11); just the CRM. + +--- + +### Phase 11: CRM Win → Auto-Provision (Final Integration) +**Duration:** 2–3 days +**Why:** The payoff: closing a deal auto-creates a client + project. + +**Tasks:** +1. Implement `winLead(leadId, quoteId)` server action (see code above): + - Create `clients` from lead data + - Create `projects` from offer title + - Copy `offer_phases` → `project.phases` + - Create `payments` (1-4 installments) + - Update lead: client_id, project_id, won_quote_id, status = "won" +2. Expose `winLead` via `/admin/leads/[id]` UI (button to manually win) +3. Trigger `winLead` automatically when quote accepted via `/api/public/quote/accept` +4. Integration test: lead → quote → accept → check auto-provisioned client + project +5. Email flow scaffold (not sent yet, but components ready) + +**Outputs:** +- `winLead` server action (core logic) +- Admin UI button + action +- Auto-trigger on quote acceptance +- End-to-end integration test + +**Milestone complete:** v2.0 Business Operations Suite fully functional. + +--- + +## Migration Strategy: Data Safety + +### Golden Rules (LOCKED) + +1. **Never delete or truncate** `clients`, `projects`, `payments`, `phases` rows +2. **All migrations additive-first** (add columns, create tables, FK links) +3. **Deprecated tables kept live** until explicit cleanup phase +4. **Rollback always possible** (audit fields + original data preserved) + +### Phase 7 Catalog Migration (SQL Example) + +```sql +-- Step 1: Create new unified services table +CREATE TABLE services ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + unit_price NUMERIC(10, 2) NOT NULL, + category TEXT, + active BOOLEAN NOT NULL DEFAULT true, + migrated_from TEXT, -- "service_catalog" | "offer_services" | null + migrated_id TEXT, -- original ID from old table + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Step 2: Migrate service_catalog → services +INSERT INTO services (id, name, description, unit_price, category, active, migrated_from, migrated_id, created_at) +SELECT id, name, description, unit_price, 'general', active, 'service_catalog', id, COALESCE(created_at, now()) +FROM service_catalog; + +-- Step 3: Migrate offer_services → services (dedup by name) +INSERT INTO services (id, name, description, unit_price, category, active, migrated_from, migrated_id, created_at) +SELECT + nanoid(), + os.name, + os.transformation_description, + os.price, + 'offer', + os.active, + 'offer_services', + os.id, + now() +FROM offer_services os +WHERE NOT EXISTS ( + SELECT 1 FROM services s WHERE s.name = os.name +); + +-- Step 4: Update quote_items FK +ALTER TABLE quote_items +DROP CONSTRAINT quote_items_service_id_fkey, +ADD CONSTRAINT quote_items_service_id_fkey + FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE RESTRICT; + +-- Step 5: Old tables marked deprecated in schema comments (not deleted) +-- No data lost; rollback possible if needed. +``` + +**Rollback safety:** Each phase includes: +- Audit columns tracing data origin +- No drops — old tables kept as fallback +- Transaction-safe (all or nothing) +- Test on staging first + +--- + +## Anti-Patterns to Avoid + +### ❌ Separate API Service for CRM +**Why bad:** Adds operational burden (another container, deploy pipeline). Single-admin overhead unjustified. +✓ **Instead:** CRM tables in same Postgres, Server Actions in same app. + +### ❌ Exposing Quote Item Details to Client +**Why bad:** Reveals internal pricing, breaks confidentiality. +✓ **Instead:** Keep quote_items admin-only. Client sees only `accepted_total`. + +### ❌ Hard-linking Offer Phases to Project Phases +**Why bad:** Can't customize project phases without affecting template. +✓ **Instead:** Copy offer_phases → project.phases (template → instance, instance editable). + +### ❌ No Migration Audit Trail +**Why bad:** Can't debug consolidation issues; rollback impossible. +✓ **Instead:** `migrated_from` + `migrated_id` on every migrated row. + +### ❌ Public Quote Routes with Session Auth +**Why bad:** Requires account/session. Defeats "send link to lead" simplicity. +✓ **Instead:** Token-gated like `/client/[token]`. Link is enough. + +--- + +## Scalability Considerations + +| Concern | At 10 Leads | At 100 Leads | At 1K Leads | Mitigation | +|---------|------------|-------------|-----------|-----------| +| Lead activities | Linear inserts | Index on lead_id + created_at | Partition by date | Phase 8 add index | +| Quote list filtering | Full scan | Add index (status, created_at) | Pagination + cache | Phase 9+ optimization | +| CRM dashboard KPIs | Fast | May timeout | Cache in settings | Phase 10 widget | +| PDF generation | Inline OK | Consider async queue | Must queue | Phase 9 scaffold | + +**Current scope (Phases 7–11):** Assume < 100 leads. Scale signals: +- Dashboard slow? → Cache KPIs in `settings` table, update on mutations +- Quote list slow? → Add indexes, implement cursor pagination +- PDF blocking? → Defer to background job queue (future) + +--- + +## Sources & Confidence Assessment + +| Area | Confidence | Source | Notes | +|------|------------|--------|-------| +| Schema design | HIGH | Existing v1.0 schema + constraints from PROJECT.md | Additive-first, migrations safe | +| Offer phases mechanics | HIGH | offer_macros/micros existing, project phases exist | Copy logic straightforward | +| Public quote routes | HIGH | Existing /client/[token] middleware pattern | Token gating proven, public variant tested | +| CRM flow | HIGH | Win→auto-provision is explicit requirement in PROJECT.md | Scope well-defined | +| API security | HIGH | v1.0 client API never exposes quotes/pricing | Pattern established; extend, don't break | +| Build order | MEDIUM | Phase dependencies mapped; some risk in phase 9 PDF generation | Recommend prototype PDF gen in Phase 9 research | + +--- + +## Key Decisions (v2.0) + +| Decision | Rationale | Outcome | +|----------|-----------|---------| +| Unified `services` table | Eliminates parallel catalog maintenance (service_catalog + offer_services) | ✓ Single source of truth, easier to manage | +| Offer phases as copyable templates | Allows on-win auto-provisioning of project phases | ✓ Reduces manual setup, ensures consistency | +| Public quote routes with token gating | Matches existing /client/[token] pattern; no session required | ✓ Simplicity, "send link to lead" flow works | +| CRM auto-provisioning on win | Closing a deal auto-creates client + project | ✓ Eliminates data entry, speeds up onboarding | +| Single app, single DB | No service separation; all routes/tables coexist | ✓ Simpler ops, single deploy, single auth model | +| Additive-first migrations | Never delete/truncate core tables; audit trail on all moves | ✓ Safe, reversible, satisfies Data Safety constraint | -- Phase 1: DB schema + token API + client portal (all three coupled) -- Phase 2: Admin auth + CRUD management + comments + approvals -- Phase 3: Service catalog + quote builder -- Phase 4 (v2): Claude onboarding flow diff --git a/.planning/research/FEATURES_v2.0.md b/.planning/research/FEATURES_v2.0.md new file mode 100644 index 0000000..8d4f3f0 --- /dev/null +++ b/.planning/research/FEATURES_v2.0.md @@ -0,0 +1,412 @@ +# Feature Landscape: Business Operations Suite v2.0 (New Features) + +**Domain:** Proposal generation + lightweight CRM for solo personal-branding consultant +**Researched:** 2026-06-10 +**Research Mode:** Ecosystem (proposal software + lightweight CRM patterns) +**Confidence:** MEDIUM-HIGH +**Scope:** ONLY new v2.0 features (does NOT review v1.0 table stakes already shipped) + +--- + +## Executive Summary + +The Business Operations Suite adds three interconnected workflows to ClientHub: + +1. **Proposal Generation & Delivery** — Sales call → multistep proposal page (2-hour delivery) → lead selects tier A/B/C → acceptance triggers automation +2. **Lead Pipeline Management** — Minimal CRM for tracking prospects through 5 stages (Contacted → Qualified → Proposal Sent → Negotiating → Won/Lost) +3. **Onboarding Automation** — When lead is marked Won, auto-create client + project with phases copied from chosen offer + configurable payment schedule + +**For a solo consultant, this is radically different from team CRMs** (Pipedrive, HubSpot, GoHighLevel). Avoid: territory management, team routing, email automation, role-based access, forecasting rollups, lead scoring algorithms. Instead: **minimal data entry, follow-up reminders based on last-contact date, and seamless handoff from proposal acceptance to project setup.** + +**Core insight:** Solo consultants manage long-term relationships across years, not one-time deals closed in 30 days. Pipeline stages should reflect relationship milestones (Contacted, Qualified, Proposal Sent), not sales rep activity (Attempted, Left Message). Follow-ups are "who did I talk to recently?" not "assign task to rep." + +--- + +## Table Stakes Features (New v2.0) + +Features users expect when adopting a proposal + CRM system. Missing these = product feels incomplete. + +### Proposal Generation & Delivery + +| Feature | Why Expected | Complexity | Notes | +|---------|--------------|-----------|-------| +| Pick client + 1–3 offers, set per-quote prices | Core value: sales call → proposal in 2 hours. Prices set per quote, not from catalog (allows price increases over time without updating service catalog) | Medium | Depends on existing `offers` & `clients` (Phase 5). Quote prices override offer base prices. | +| Generate public multistep HTML/CSS page | Leads need clickable link, not PDF email. Interactive engagement beats static documents (Qwilr/Proposify standard). Reduces abandonment. | Medium | Responsive, mobile-first, branded with consultant name/logo. No login required. | +| Multistep flow (intro → pricing tiers → CTA) | Reduces cognitive overload vs. single long page. Step 1: offer overview. Step 2: A/B/C tiers with pricing. Step 3: accept/decline button. | Low | Conditional logic hides irrelevant sections; simple progressive disclosure. | +| Public acceptance button (no e-signature) | Lead confirms tier choice WITHOUT entering hub. Triggers: timestamp capture + email record + lead status update. | Low | Simple "Accept this offer" button → records accepted_at + accepted_by_email. E-signature deferred (design ready, v1 constraint). | +| Proposal URL shareable & public | Lead receives link in email, opens in browser, no friction. | Low | Generate unique slug (`/proposal/[uuid]`). No auth. Optionally expires after N days or on acceptance. | +| Acceptance proof (signer + timestamp) | Documentation for dispute resolution. Downloadable PDF or stored audit record. | Low | Store: accepted_by_name, accepted_by_email, accepted_at, accepted_offer_id. | + +### Lead Pipeline + +| Feature | Why Expected | Complexity | Notes | +|---------|--------------|-----------|-------| +| Pipeline stages: Contacted → Qualified → Proposal Sent → Negotiating → Won/Lost | Core tracking. Each stage requires buyer action, not just rep activity. Termination: Won or Lost. | Low | **Minimal for solo:** 5 stages max. More = data entry overhead. Stages are immutable enums. | +| Move lead between stages (UI) | Basic pipeline UX. See all leads grouped by stage at a glance. | Low | Dropdown per row (simple) or kanban board (polish). Dropdown sufficient for MVP. | +| Lead details: name, email, company, phone, last_contact_date, next_action, notes | Minimum context for follow-ups. | Low | Notes = freeform text (call outcomes, concerns, personality notes, next steps). | +| Lead created manually OR auto-created from proposal send | Manual: import prospects. Auto-create: when proposal sent to unknown email, create lead record automatically. | Low | Auto-create is convenience; manual is fallback. Both supported. | +| Log activities: calls, emails, meetings, notes | Track interactions without leaving dashboard. Auto-updates last_contact_date. | Low | Activity types: [call, email, meeting, note]. Log date, duration, description. Displayed as feed. | + +### Follow-Up Reminders + +| Feature | Why Expected | Complexity | Notes | +|---------|--------------|-----------|-------| +| Dashboard widget: "Follow up today" sorted by last_contact_date | Solves core pain: "who did I talk to recently that I should check in with?" | Low | Query: WHERE last_contact_date <= TODAY - 7 days AND stage NOT IN (Won, Lost). Red badge. | +| Proposal stall detector: if Proposal Sent >3 days, no response | Flags silence = red flag for follow-up. | Low | Simple dashboard alert; no email automation (consultant checks dashboard in morning). | +| Auto-update last_contact_date on activity | Keeps reminders accurate. Prevents "I called them but forgot to log it" drift. | Low | Trigger: on call/email/meeting/note creation OR stage change → update lead.last_contact_date = NOW. | + +### Project Auto-Creation (Won → Onboarding) + +| Feature | Why Expected | Complexity | Notes | +|---------|--------------|-----------|-------| +| On lead.stage = Won: auto-create `client` + `project` with copied phases | Closing automation. Proposal acceptance → instant hub readiness for delivery. | Medium | Requires: chosen offer known, project_offers finalized (Phase 5), 1-4 configurable installments. | +| Copy offer phases → project phases (modifiable) | Project inherits offer structure but can be customized per client without mutating offer template. | Medium | Phases are modifiable in hub (Phase 1). Creation source logged: "Copied from Offer X on [date]" for audit. | +| Set payment installments per project (not per offer) | Payment plan configured case-by-case: some 50/50, others 4-part. Allows flexibility. | Medium | Templates: 50/50 (acconto/saldo), 3-part, 4-part, custom. UI: choose template during Won → project creation. | +| Lead → Client transition: copy email, create token, link to existing client if repeat | Avoid duplicate clients. Allow linking Won lead to existing client (repeat engagement). | Medium | Check email collision. If exists: "Add project to existing client X?" vs. "Create new client". Generate secret token. | + +--- + +## Differentiators (New v2.0) + +Features that set ClientHub apart from generic proposal software + CRM. Not expected, but valued for solo consultant use case. + +| Feature | Value Proposition | Complexity | Notes | +|---------|-------------------|-----------|-------| +| Tier-independent offer structure (Signature A/B/C as separate offers) | Each tier has own URL, phases, pricing. A = professional, B = budget—same catalog, different scope. No cloning, no inheritance logic. | Medium | Requires flexible offer builder (drag services between phases), offer status (draft/active/archived). Built in Phase 5. | +| Proposal phases visible in public page (lead sees what they're buying) | Lead sees not just price, but scope: "4-week branding sprint, 3 revision rounds, monthly retainer". Transparency builds confidence. | Low | Proposal page shows phase names, deliverables summary (text), timeline (start + duration). | +| Dashboard follow-up list filterable by offer type + stage | "Show all Signature A leads in Negotiating" to spot bottlenecks by tier. | Low | Filter buttons: by offer type, stage, last_contact_date range. Dropdown or toggle buttons. | +| Activity feed per lead (call notes, email log, meetings, searchable) | Context without leaving dashboard. "Last contact: 2026-05-28, 11am call—timeline concerns". | Medium | Depends on activity logging system. Store type, date, duration, description. Searchable. | +| Proposal pages branded with consultant info (not generic SaaS) | Proposal is part of sales process; branding reinforces personal brand. | Low | Logo, consultant name, brand colors, professional typography inherited from admin settings. | +| Email integration hint (design ready, defer to later batch) | Send proposal via dashboard; auto-log email send as activity. | Medium | Deferred per PROJECT.md (v1 constraint). Placeholder: manual copy-paste OK for MVP. | + +--- + +## Anti-Features: What NOT to Build (v2.0) + +| Anti-Feature | Why Avoid | What to Do Instead | +|--------------|-----------|-------------------| +| Multi-user team collaboration, role-based access | Solo consultant is single admin. Team features add auth complexity, sync issues, notification storms. Overhead >> value. | Stay single-admin. Architecture with permission flags but don't build UI/logic. If team joins later, migrate incrementally. | +| CRM email automation, drip campaigns, email sequences | Solo consultant sends proposals + checks in manually. Automation-heavy workflows are for outbound prospecting funnels (not this use case). Lead generation not in scope. | Keep simple: dashboard reminder = "check in with X". Consultant sends email manually, logs it as activity. | +| Forecasting with rollups, quota tracking, team capacity planning, territory assignment | These are team sales metrics. Solo consultant cares: "How many leads in pipeline?" and "What's revenue if all Negotiating deals close?" | Simple dashboard: shows total value by stage (e.g., "Negotiating: $45k"), breakdown by offer type. No forecast math. | +| Lead scoring, MQL → SQL qualification algorithms, scoring rules | Solo consultant qualifies leads manually (call + gut feel). Scoring requires training data and tuning. | Keep manual: stage = Contacted, Qualified, Proposal Sent. "Qualified" is consultant's call. No automation. | +| Calendar sync, email sync, call recording, Slack/Teams integration | Adds external dependency surface area. Consultant logs calls + emails manually. | Fallback: freeform "Call notes" text field per activity. No attempt to auto-log from email/Slack. | +| Multi-currency support, tax calculation, invoice generation, accounting system integration | Accounting is out of scope (PROJECT.md locked). Payment tracking only. | Keep simple: amount_accepted in EUR, payment schedule in local currency. No conversion logic or invoicing. | +| Proposal version control, edit history, detailed audit trail | Overkill for solo consultant. One proposal per client, accept or decline. | Simple: created_at, updated_at timestamps. Accepted version is locked (immutable once accepted_at set). No versioning. | +| Client can reverse/un-approve acceptance | Creates ambiguity in closing. | Once accepted_at is set, proposal is final. Consultant can move lead back to Proposal Sent if needed. | + +--- + +## Feature Dependencies (v2.0) + +``` +Proposal Generation: + ├─ clients (exists, Phase 1) + ├─ projects (exists, Phase 1) + ├─ offers (Phase 5: id, name, offer_type [Entry/Signature/Retainer/custom], created_at) + ├─ services catalog (Phase 3: id, name, price, duration_days) + └─ offer_services (Phase 5: junction table, offers ↔ services) + +Lead Pipeline & CRM: + ├─ leads table (NEW: id, name, email, company, phone, created_at, last_contact_date, stage, offer_id FK, next_action text) + ├─ activities table (NEW: id, lead_id FK, type enum [call/email/meeting/note], log_date, description, duration_minutes) + ├─ lead_stage enum (NEW: Contacted, Qualified, Proposal Sent, Negotiating, Won, Lost) + +Project Auto-Creation (Won → Onboarding): + ├─ leads.stage = Won (from pipeline, not auto-set) + ├─ projects (exists, must be creatable via API/trigger) + ├─ phases (exists, must be copyable from offer phases) + ├─ payments (Phase 5: must support project_id FK, amount, due_date, status) + └─ clients.token (exists, Phase 1/4: rotatable secret) + +Proposal Public Pages: + └─ proposals table (NEW: id, client_id, lead_email, offers [JSON array], created_at, public_url, accepted_at, accepted_by_name, accepted_by_email, accepted_offer_id) +``` + +**Critical dependency:** Phase 5's `project_offers` relationship must be finalized. If not, proposal generation cannot proceed. Phase 7 proposal work is blocked by Phase 5 completion. + +--- + +## Feature Categorization by Component + +### Component 1: Proposal Builder & Public Pages + +**Responsibility:** +- Admin UI: select client, pick 1–3 offers, confirm per-quote pricing, set lead email, generate shareable link +- Public page: multistep form (intro → tiers → CTA), responsive, no login, branded +- Acceptance: simple "Accept" button, no e-signature +- Database: `proposals` table + +**Features in scope:** +- Proposal generation from client + offers + prices +- Public multistep page with interactive tier selection +- Conditional phase visibility (show/hide sections based on tier) +- Acceptance button + timestamp + email capture +- Auto-create lead on proposal send (if lead email not in DB) +- Proposal expiry (optional, default: never) + +**Database schema:** +``` +proposals: + id UUID PK + client_id FK → clients + lead_email VARCHAR (recipient) + offer_ids JSON array (which offers are included) + quote_prices JSON {offer_id: price} (per-quote override) + created_at timestamp + updated_at timestamp + public_url slug + accepted_at timestamp nullable + accepted_by_name VARCHAR nullable + accepted_by_email VARCHAR nullable + accepted_offer_id FK nullable (which tier was chosen) +``` + +### Component 2: Lead Pipeline & Dashboard + +**Responsibility:** +- Lead table with name, email, company, stage, last_contact_date, notes +- Dashboard: table or kanban view of all leads grouped by stage +- Activities: log calls, emails, meetings, notes +- Follow-up reminder widget: leads not contacted in 7+ days + +**Features in scope:** +- Create/edit/delete leads manually or auto-create from proposal +- Move lead between stages (dropdown or drag/drop) +- Log activities: type [call/email/meeting/note], duration, description +- Auto-update last_contact_date on activity +- Follow-up reminders: "Follow up today" widget, proposal stall detector +- Lead notes: freeform text, searchable +- Filter by offer type, stage, date range +- Activity feed per lead (searchable) + +**Database schema:** +``` +leads: + id UUID PK + name VARCHAR + email VARCHAR (unique or indexed) + company VARCHAR nullable + phone VARCHAR nullable + stage enum [Contacted, Qualified, Proposal Sent, Negotiating, Won, Lost] + offer_id FK → offers nullable (which offer was sent) + created_at timestamp + last_contact_date date + next_action text nullable (next step consultant plans) + notes text (accumulated call notes, concerns, personality) + +activities: + id UUID PK + lead_id FK → leads + type enum [call, email, meeting, note] + log_date timestamp + duration_minutes INT nullable + description text + created_at timestamp +``` + +### Component 3: Project Auto-Creation & Onboarding Flow + +**Responsibility:** +- On lead.stage = Won, trigger creation: new client (if not exists), new project, copy phases, set payment schedule +- UI: lead moves to Won → modal: "Which offer was chosen?" → "Confirm project creation?" → choose payment template → create +- Result: lead archived, project appears in /admin/projects, client gets dashboard URL + +**Features in scope:** +- Auto-create client from lead details +- Auto-create project with copied phases +- Copy phases from offer (modifiable in hub) +- Payment installments: choose template → creates N payment records +- Generate client secret token + dashboard URL +- Optional: link to existing client instead of creating new +- Notification: "Lead X → Project Y created on [date]" + +**Database schema:** +``` +No new tables needed. Uses existing: clients, projects, phases, payments. +When lead → Won: + 1. Check if lead.email in clients table + 2. If not: create new client, generate secret token + 3. If yes: ask to link to existing client or create new + 4. Copy phases from offer_id to new project + 5. Create N payment records based on chosen template + 6. Update lead.stage = Won (already set by UI) + 7. Record: project creation_source = "Lead X, offer Y" +``` + +--- + +## MVP Recommendation + +### Phase 7 (MVP): Core Proposal Generation + Basic Pipeline + +**Priority 1: Must-Have (Launch features)** + +1. **Proposal builder UI:** client + offer(s) + per-quote prices → generate public multistep page +2. **Public proposal page:** Step 1 = offer overview, Step 2 = A/B/C tiers + pricing, Step 3 = "Accept this Offer" CTA +3. **Acceptance flow:** "Accept" button → timestamp + email capture, marks proposal as accepted +4. **Lead auto-creation:** when proposal sent to unknown email, auto-create lead record +5. **Basic lead pipeline:** Contacted, Qualified, Proposal Sent, Negotiating, Won, Lost stages +6. **Lead dashboard:** table view, move stage via dropdown, lead details sidebar +7. **Follow-up reminder widget:** leads not contacted in 7+ days, sorted by last_contact_date, dashboard badge +8. **Activity logging (basic):** create call/email/meeting records, auto-update last_contact_date + +**Estimated effort:** 5–7 days (proposal builder UI + public page + lead CRUD + follow-up reminders) + +**Launch readiness:** Solo consultant can generate proposal in 2 hours, track lead through pipeline, see follow-up reminders. + +--- + +### Phase 8 (Enrichment): Lead Context & Polish + +**Priority 2: Features** + +1. **Lead notes:** freeform text per lead, searchable +2. **Activity feed:** display recent activities per lead (call, email, meeting, note), searchable +3. **Dashboard filters:** by offer type, stage, date range (last_contact_date) +4. **Proposal stall detector:** flag if Proposal Sent >3 days with no activity +5. **Lead detail card:** expand to show full context (notes, activities, next_action) without leaving dashboard + +**Estimated effort:** 3–4 days + +**Value:** Consultant has full context for each lead without switching views. + +--- + +### Phase 9+ (Automation & Onboarding): Project Auto-Creation + +**Priority 3: Features** + +1. **Project auto-creation:** on lead → Won, auto-create client + project with copied phases +2. **Copy phases from offer:** phases inherit name, duration, deliverables from offer template; modifiable in hub +3. **Payment installments:** choose template (50/50, 3-part, 4-part, custom) → create N payment records +4. **Client token generation:** auto-generate secret token, send dashboard URL to client +5. **Duplicate client check:** if lead.email exists in clients, ask to link or create new +6. **Won lead notification:** dashboard shows "Lead X → Project Y created", activity logged + +**Estimated effort:** 3–4 days + +**Value:** Closing automation. Lead acceptance → instant project setup in hub, ready for delivery. + +--- + +## Complexity Assessment (Effort & Maintenance) + +| Feature | Dev Effort | Maintenance Burden | Deferability | Phase | +|---------|-----------|-------------------|----------------|-------| +| Proposal generation (builder UI + logic) | 2–3 days | Low (static) | No—core | 7 | +| Public multistep page (HTML/CSS template) | 1–2 days | Low (static) | No—core | 7 | +| Acceptance flow (button + timestamp) | 1 day | Minimal | No—core | 7 | +| Lead pipeline (CRUD + stage transitions) | 1 day | Low | No—core | 7 | +| Follow-up reminders (dashboard query) | 4 hours | Minimal | No—core | 7 | +| Activity logging (calls, emails, meetings) | 1 day | Low | Yes—Phase 8 | 8 | +| Activity feed (search + timeline) | 1 day | Low | Yes—Phase 8 | 8 | +| Lead notes + search | 1 day | Low | Yes—Phase 8 | 8 | +| Dashboard filters (offer type, stage, date) | 1 day | Low | Yes—Phase 8 | 8 | +| Project auto-creation (trigger + cascade) | 2 days | Medium (error handling) | Yes—Phase 9 | 9 | +| Payment installments (templates) | 1 day | Low | Yes—Phase 9 | 9 | +| Client token generation + link | 4 hours | Low | Yes—Phase 9 | 9 | +| Duplicate client check | 4 hours | Low | Yes—Phase 9 | 9 | +| Email integration (send proposal, auto-log) | 2–3 days | Medium (provider) | Yes—defer | Later | +| Multi-user + team features | 3–5 days | Medium-High (roles) | N/A—anti-feature | Never | + +**Total Phase 7 MVP:** ~5–6 days +**Total Phases 7–9:** ~11–14 days (complete suite) + +--- + +## Solo Consultant Design Constraints + +### What's Different from Team CRMs + +| Constraint | Implication | Design Pattern | +|-----------|-----------|-----------------| +| Single admin, no team routing | No "assign to rep" or "owner" field. All leads = Simone. | All leads belong to admin; no ownership concept. No role-based UI. | +| Relationship-heavy, not transaction-heavy | Leads stay in pipeline for months, not 30 days. | Stage = relationship milestone (Contacted, Qualified, Proposal Sent), not rep activity (Attempted, Left Message, Following Up). | +| Qualitative follow-up, not automation | "Check in with X, heard timeline concerns" not email sequences. | Notes + activity log, no email automation. Manual send, logged as activity. | +| Low deal volume (5–15 in flight) | Pipeline visualization doesn't need advanced analytics. | Simple table or 5-column kanban board. No forecasting rollups. | +| Minimal data entry tolerance | Won't log every call if it takes >3 clicks. | Modal: "Log call → name/date/notes → save" in <10 seconds. | +| No role-based access | No "manager sees forecast, rep sees pipeline". | Single "admin dashboard" with all data. No permission layers. | +| No forecasting pressure | Consultant forecasts deal value by feel, not rollup math. | Dashboard shows "Total Negotiating: $45k". That's it. No probability weighting. | +| Calendar/email often offline (field work) | Email sync is nice-to-have, not must-have. | Fallback: manual entry. No real-time sync requirement. | +| Accountability is personal, not hierarchical | No "why is this deal stalled?" escalations. | Follow-up reminder = consultant decides action. No automatic escalations. | + +### UX Patterns for Solo Consultant + +1. **Minimize data entry by default:** Yes/No confirmations, checkboxes, dropdowns. Avoid text fields unless essential. +2. **Context visible by default:** Open a lead → see last 3 activities, next_action, stage, offer type in one view. No tabs. +3. **Dashboard as command center:** Lead reminders + upcoming payments + next meetings all visible. No app switching. +4. **Proposal as shared artifact:** Lead sees what they're buying (phases, deliverables, price). Consultant sees acceptance status + lead email. +5. **Automation where it reduces clicks:** Accept proposal → auto-create lead. Move to Won → ask once for payment plan, auto-create project. Don't auto-email. + +--- + +## Pipeline Benchmark (Reference) + +From industry research: + +| Metric | Benchmark | Implication for ClientHub | +|--------|-----------|---------------------------| +| Typical pipeline stages | 5–7 stages | ClientHub uses 5: Contacted, Qualified, Proposal Sent, Negotiating, Won/Lost. Minimal overhead. | +| Proposal Sent → Negotiation conversion | 40%+ | Flag stale proposals (>3 days, no activity) in dashboard. | +| Negotiation → Won conversion | 50%+ | If low, consultant reviews proposal scope or pricing. | +| Lead follow-up frequency | 8+ touches to close | Activity logging tracks touches (calls, emails, meetings). No automation; manual follow-up. | +| Typical pipeline review cadence | Weekly or bi-weekly | Consultant checks dashboard daily; reviews pipeline health weekly. | +| Average deal cycle for consultants | 30–90 days | ClientHub supports long cycles (no pressure to close fast). | + +--- + +## Confidence & Sources + +| Area | Confidence | Basis | Sources | +|------|------------|-------|---------| +| Proposal software features (multistep, acceptance, tiering) | HIGH | Verified with Qwilr, PandaDoc, Proposify; 2026 comparison articles | [Qwilr vs PandaDoc](https://www.proposify.com/blog/qwilr-vs-pandadoc), [PandaDoc vs Proposify vs Qwilr](https://saas-tools.medium.com/pandadoc-vs-proposify-vs-qwilr-which-proposal-tool-is-worth-your-budget-in-2026-8e8339d6ab87), [8 best proposal software](https://www.getaccept.com/blog/proposal-software) | +| Pipeline stage structure (5–7 stages, Won/Lost termination) | HIGH | Verified with Pipedrive, Capsule CRM, Salesforce; "Proposal Sent → Negotiation → Won" is standard | [Salesforce Pipeline Management](https://www.salesforce.com/sales/pipeline/management/), [CRM Pipeline Stages](https://prospeo.io/s/crm-pipeline-stages), [GoHighLevel Pipeline](https://ecosire.com/blog/ghl-crm-pipeline-management) | +| Lead follow-up reminders (last_contact_date, 7+ days flagged) | MEDIUM-HIGH | Sourced from Outreach, HubSpot, Nimble best practices; solo consultant context is reasonable inference | [Sales Pipeline Best Practices](https://www.nimble.com/blog/best-practices-of-sales-pipeline-management/), [Outreach Pipeline Management](https://www.outreach.ai/resources/blog/sales-pipeline-management-best-practices) | +| Solo consultant CRM avoidance of team features | MEDIUM | Sourced from "Best CRM for Solopreneurs" guides; solo preference for simplicity consistent across 3+ sources | [Breakcold: CRM for Consultants](https://www.breakcold.com/blog/crm-for-consultants), [Authencio: CRM for Freelancers](https://www.authencio.com/blog/best-crm-for-consultants-freelancers-guide), [Addtocrm: Best CRM Solopreneurs](https://addtocrm.com/tools/best-crm-for-solopreneurs), [Mimiran: Anti-CRM](https://www.mimiran.com/fun-crm-for-solo-consultants-who-hate-selling/) | +| Tiered proposal (A/B/C independent offers) | MEDIUM-HIGH | Verified via multiple consulting pricing guides; "3 tiers ideal" is consensus. Independent offers is ClientHub-specific. | [Mercury: Pricing Strategy](https://mercury.com/blog/pricing-strategy-consulting), [Ignition: Tiered Pricing](https://www.ignitionapp.com/blog/tiered-pricing-strategy-for-professional-services-proposal-templates), [Consulting Success: Consulting Rates](https://www.consultingsuccess.com/consulting-rates) | +| Multi-step form engagement patterns | HIGH | Verified with Heyflow, Optimonk, HubSpot research; form completion rates improve with progressive disclosure | [Instapage: Multi-Step Forms](https://instapage.com/blog/multi-step-forms), [HubSpot: Multi-Step Forms](https://blog.hubspot.com/marketing/multi-step-forms), [Webstacks: Multi-Step Form Examples](https://www.webstacks.com/blog/multi-step-form) | +| Proposal acceptance automation (→ project creation) | MEDIUM | Sourced from Anchor, Estimate Rocket, Monograph workflows. ClientHub implementation is custom but pattern is established. | [Sayanchor: Proposal Acceptance](https://www.sayanchor.com/post/proposal-acceptance-guide), [Monograph: Build, Send, Sign](https://monograph.com/blog/build-send-and-sign-proposals-with-pipeline), [DocuSign: Workflow Automation](https://www.docusign.com/blog/workflow-automation-electronic-signatures) | + +--- + +## Open Questions for Later Phases + +1. **Email integration:** Should proposals be sent via dashboard integration, or manual copy-paste? (Deferred to Phase 10+) +2. **Proposal expiry:** Should proposals auto-expire after N days, or stay open indefinitely? (MVP: no expiry) +3. **Lead de-duplication:** If lead email is already in clients table, how should system handle it? (MVP: manual check; Phase 9+ auto-detect) +4. **Activity type richness:** Should activities include location, participant names, sentiment tags? (MVP: basic type + notes; Phase 8 can enhance) +5. **Proposal versioning:** Can consultant send multiple proposals to same lead? (MVP: yes, same lead.email + new proposal record) +6. **Payment plan customization:** Can consultant define custom installment schedules, or only use templates? (MVP: templates; Phase 9 can add custom) +7. **Lead archive vs. delete:** When lead is Won → converted to client/project, should lead record stay in DB for history? (Recommendation: keep for audit trail; mark archived or moved_to_project_id) + +--- + +## Appendix: Frequently Asked Questions + +**Q: Why no e-signature in MVP?** +A: Design is ready (CLAUDE.md notes it as deferred). A simple "Accept" button + timestamp is sufficient proof of acceptance for solo consultant use case. E-signature (DocuSign, Stripe Sign) adds external dependency and cost; defer unless client explicitly requests. + +**Q: Should proposals auto-expire?** +A: No, not in MVP. Solo consultant may send proposal Monday, follow up Thursday, acceptance Friday. Expiry is team-sales friction (forces re-quote). Keep simple: accepted_at = null means still open. + +**Q: Can a lead be in two stages at once?** +A: No. Stage is singular immutable state at any moment. If second proposal sent to same lead: either (a) create new lead record (same email, add suffix "2"), or (b) update same lead.stage to "Proposal Sent" again. Option (b) is simpler; stage can repeat; last_contact_date updates on each proposal send. + +**Q: Should activities auto-log from email/Slack?** +A: No, not in MVP. Adds external dependency surface. Fallback: consultant manually logs calls/emails as activities in <10 seconds. Acceptable for solo consultant volume (5–15 leads). + +**Q: What if lead email already exists in clients table?** +A: Phase 9 auto-detection: when moving lead to Won, check if lead.email in clients. If yes, ask: "Link to existing client X (repeat engagement)?" or "Create new client". Prevents duplicate records. + +**Q: Can phases be reordered after project creation?** +A: Yes. Phases are copied from offer at project creation time, but project phases are modifiable in hub (Phase 1, already built). Offer template is never mutated. Studio-grade immutability. + +**Q: What's the payment flow after Won → project creation?** +A: Consultant chooses payment template (50/50, 3-part, 4-part, custom) during Won → project creation modal. System creates N records in `payments` table with due_dates calculated from project start_date. Client sees payment schedule in hub dashboard (Phase 1, already built). No invoicing or collection automation (out of scope, PROJECT.md locked). + +**Q: Can consultant change their mind after accepting a proposal?** +A: If consultant moves lead back to Proposal Sent or Negotiating (after accepting and creating project), the original accepted_proposal record remains immutable. Project can be deleted/archived if needed, but proposal acceptance is final. This matches real-world: once accepted, consultant can't un-close a deal. + +--- + +**End of FEATURES_v2.0.md** diff --git a/.planning/research/STACK.md b/.planning/research/STACK.md index db9dc7f..d21f785 100644 --- a/.planning/research/STACK.md +++ b/.planning/research/STACK.md @@ -1,132 +1,391 @@ -# Technology Stack — ClientHub Freelancer Client Portal +# Technology Stack — ClientHub v2.0 Business Operations Suite -**Project:** ClientHub (welcomeclient.iamcavalli.net) -**Researched:** 2026-05-09 -**Confidence:** HIGH +**Project:** ClientHub +**Current Version:** 1.0 (Production: hub.iamcavalli.net on Coolify/Hetzner) +**Researched:** 2026-06-11 +**Scope:** Stack additions for v2.0 features: drag-and-drop offer builder, public multistep quote pages, CRM lead pipeline, follow-up reminder system. --- -## Recommended Stack +## Existing Stack (v1.0 — Validated) -### Core Framework - -| Technology | Version | Purpose | Why | -|------------|---------|---------|-----| -| Next.js | 15.x (latest stable) | Full-stack app framework | App Router + Server Actions replace a separate API layer. Vercel-native: no adapter needed. First-class TypeScript. | -| React | 19.x | UI rendering | Bundled with Next.js. Server Components eliminate client-side waterfalls for the read-heavy client portal. | -| TypeScript | 5.x | Type safety | Drizzle + Zod give end-to-end type inference from DB schema to form validation. | - -**Why NOT Remix / SvelteKit / Astro:** They work on Vercel but add unfamiliarity overhead with no gain at this scale. +| Technology | Version | Status | Role | +|------------|---------|--------|------| +| Next.js | 16 | Production | App Router, Server Actions | +| React | 19 | Production | UI rendering | +| TypeScript | 5.x | Production | Type safety (strict mode) | +| Postgres | (Neon) | Production | Primary database | +| Drizzle ORM | Latest | Production | DB access + migrations | +| Auth.js | v4 | Production | Admin session + token middleware | +| Tailwind CSS | v4 | Production | Utility-first styling | +| shadcn/ui | (latest) | Production | Component library (copied) | +| Zod | Latest | Production | Validation schemas | +| nanoid | Latest | Production | Token generation | --- -### Database +## New Stack Additions for v2.0 -| Technology | Purpose | Why | -|------------|---------|-----| -| Neon (serverless Postgres) | Primary database | Free plan: 0.5 GB storage + 100 CU-hours/month — sufficient for 5–20 clients. Scales to zero between uses. Native Vercel integration that auto-injects DATABASE_URL per preview branch. | -| Drizzle ORM | DB access + migrations | Lightest-weight TS ORM. Ships `drizzle-orm/neon-http` serverless driver — no persistent TCP connections, works in Vercel Node and Edge runtimes for free. Schema-as-code with `drizzle-kit` handles migrations. | +### 1. Drag-and-Drop Offer Builder (Services Between Phases) -**Why NOT Prisma:** Needs PgBouncer or Prisma Accelerate ($) for serverless connection pooling. Drizzle's `neon-http` handles this natively at zero cost. +**Requirement:** Move services between offer phases, visual reordering, keyboard + mouse support. -**Why NOT Supabase:** Adds RLS, Realtime, and Auth overhead you don't need and will have to maintain. +| Technology | Version | Purpose | Why This Choice | Status | +|------------|---------|---------|-----------------|--------| +| `@dnd-kit/react` | ^10.0.0+ | Drag-drop core + React hooks | Modern, React 19 verified compatible. Official support via `@dnd-kit/react` package (not legacy `@dnd-kit/core`). Built-in PointerSensor (mouse/touch/pen) + KeyboardSensor. Zero peer dependency conflicts with Next.js 16 + React 19. | RECOMMENDED | +| `@dnd-kit/helpers` | ^10.0.0+ | move(), swap() utilities | Bundles with React pkg; simplifies reordering logic. | RECOMMENDED | + +**Why NOT Alternatives:** +- `@hello-pangea/dnd` (v16–17): Locks peer dependency to React 18.x. Official React 19 support not released as of June 2026. Community testing shows it *may* work (with peer dependency override), but not officially supported. +- `react-beautiful-dnd`: Archived since 2022. Unmaintained. +- `pragmatic-drag-and-drop` (Atlassian): Low-level browser API attachment (manual useEffect in each component). Higher boilerplate than dnd-kit for simple offer builder. Optimized for enterprise real-time collaboration (not your use case). +- Native HTML5 Drag & Drop API: Browser-native but poor accessibility, inconsistent across browsers, verbose API. + +**Installation:** +```bash +npm install @dnd-kit/react @dnd-kit/helpers +``` + +**Integration Point:** +Wrap offer builder section with `DragDropProvider`, configure sensors for accessibility: +```typescript +import { DragDropProvider } from '@dnd-kit/react'; +import { PointerSensor, KeyboardSensor } from '@dnd-kit/dom'; + + + {/* Phases with draggable services inside */} + +``` + +**Confidence:** HIGH — dnd-kit v10+ officially supports React 19. Verified via Context7 documentation. --- -### Authentication +### 2. CRM Lead Pipeline UI (Kanban Board + Table) -| Technology | Purpose | Why | -|------------|---------|-----| -| Auth.js (next-auth) v4 | Admin session management | Credentials provider with a single admin account. Session stored as signed JWT cookie. No user table in DB. | -| Next.js Middleware (custom) | Client secret-link validation | Each client has a `secretToken` (nanoid, 21 chars) stored in DB. Middleware reads `[token]` path segment, validates against Neon, returns 404 on miss. Runs at the edge before any page renders. | -| nanoid | Token generation | Cryptographically secure, URL-safe, 21-char default (~126 bits of entropy). Generated once at client creation. | +**Requirement:** Lead stages (Qualificato → Preventivato → Vinto/Perso), drag-drop between columns, sortable lead table with filters. -**Auth flow summary:** -- `/admin/*` → Auth.js session required (single admin account) -- `/c/[token]/*` → Middleware validates token against Neon, 404 on miss -- Client pages: zero auth library overhead +| Technology | Version | Purpose | Why This Choice | Status | +|------------|---------|---------|-----------------|--------| +| `@tanstack/react-table` | ^8.0.0+ | Headless table engine | Lightweight, zero UI opinions. Composes perfectly with shadcn/ui. Powers sorting, filtering, row selection on lead list. No Material UI lock-in. Industry standard. | RECOMMENDED | +| `@dnd-kit/react` | (same as above) | Kanban column reordering | Reuse same DragDropProvider from offer builder for lead status columns. | RECOMMENDED | + +**Why NOT Alternatives:** +- `Material React Table`: Locks you into Material UI styling (conflicts with Tailwind v4). +- Pre-built kanban packages (React Big Calendar, react-kanban, etc.): Opinionated styling overhead. dnd-kit + shadcn handles it lighter. +- `TanStack Query` (React Query): Not needed here. Data is server-fetched once per page load, then local state. Add if you need sync polling later. + +**Installation:** +```bash +npm install @tanstack/react-table +``` + +**Integration Point:** +```typescript +import { useReactTable, getCoreRowModel } from '@tanstack/react-table'; + +const table = useReactTable({ + data: leads, + columns: [/* name, email, stage, last_contact, next_reminder */], + getCoreRowModel: getCoreRowModel(), +}); +``` + +**Confidence:** HIGH — TanStack Table is framework-agnostic, tested with shadcn/ui in hundreds of productions. --- -### UI +### 3. Public Multistep Quote Pages (Form + Validation) -| Technology | Purpose | Why | -|------------|---------|-----| -| Tailwind CSS v4 | Utility-first styling | CSS-first configuration, zero runtime overhead. | -| shadcn/ui | Component library | Components copied into codebase (no runtime dep). Built on Radix UI (accessible). Provides: Badge, Progress, Card, Dialog, Table, Textarea, Select. | -| lucide-react | Icons | Tree-shaken, SVG-based, consistent. | +**Requirement:** 3–5 step wizard (select client → select offer tier → review pricing → summary), per-step validation, no authentication. + +| Technology | Version | Purpose | Why This Choice | Status | +|------------|---------|---------|-----------------|--------| +| `react-hook-form` | ^7.0.0+ | Form state + client validation | You already know this pattern (used in admin). Integrates seamlessly with Server Actions. Minimal bundle (8kb gzip). Use `zodResolver` for Zod bridge. | RECOMMENDED | +| `@hookform/resolvers` | ^3.0.0+ | Zod ↔ RHF integration | 1kb addon. Single source of truth: write Zod schema once, use client (resolver) + server (Server Action). | RECOMMENDED | +| Zod | (existing) | Shared validation schema | Reuse existing Zod setup. No new dependency. Quote schema: `client_id`, `selected_offers[]`, `tier_selection`, `pricing_overrides`. | RECOMMENDED | + +**Why NOT Alternatives:** +- `Conform`: Newer, Server Action-native, requires more setup for multi-step state management. Overkill for a simple quote form. +- `Formik`: Maintenance mode since 2022. RHF is lighter + actively maintained. +- No library (plain form + `
`): Fine for very simple forms, but multi-step wizard needs state management. RHF handles this elegantly. + +**Installation:** +```bash +npm install react-hook-form @hookform/resolvers +``` + +**Integration Pattern:** +```typescript +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +const quoteSchema = z.object({ + client_id: z.string().uuid(), + selected_offers: z.array(z.string().uuid()).min(1), + pricing_tier: z.enum(['entry', 'signature', 'retainer']), +}); + +export default function QuoteForm() { + const form = useForm({ + resolver: zodResolver(quoteSchema), + mode: 'onChange', // validate on each keystroke for UX + }); + + async function onSubmit(data: z.infer) { + const result = await createQuote(data); // Server Action, server-side Zod validation + } + + return ...; +} +``` + +**Quote Page Strategy:** +- 3–5 steps total. ~2–3 fields per step. +- Validate only the *current step* before advancing (UX: don't block with future validation). +- Final submit: Server Action runs full Zod validation server-side (belt-and-suspenders). +- Use shadcn/ui Form, Input, Select, Button components (already in your stack). + +**Confidence:** HIGH — RHF + Zod + Server Actions is the 2025–2026 standard for Next.js 15+ App Router. Multiple production examples verified. --- -### Forms and Validation +### 4. Follow-Up Reminder System (Scheduled Jobs) -| Technology | Purpose | Why | -|------------|---------|-----| -| Zod | Schema validation | Server-side in Server Actions + client-side with RHF resolver. Single source of truth for data shapes. | -| React Hook Form | Admin form state | Complex admin forms (client onboarding, task editing, quote builder). Client-facing pages use native `
` + Server Actions. | +**Requirement:** "Follow-up in 3 days", "Check proposal expiry", compute when to show "needs follow-up" in dashboard, persist jobs across server restarts. + +| Technology | Version | Purpose | Why This Choice | Status | +|------------|---------|---------|-----------------|--------| +| `bullmq` | ^5.0.0+ | Job queue + time-based scheduling | Redis-backed. Jobs survive server restarts (persisted in Redis). Supports delays (`{ delay: msUntilTomorrow }`), recurring cron jobs, and job flows. Production-grade SaaS reminder standard. | RECOMMENDED | +| Redis | ^5.0.0+ (server) | Job storage + queue backend | Your Coolify stack likely has Redis. BullMQ requires it. Same instance can be used for caching, sessions, or other purposes. | REQUIRED | + +**Why NOT Alternatives:** +- `node-cron`: Runs in-process. Jobs lost on server restart. Dangerous for production reminders. +- `Agenda`: Requires MongoDB. Adds infrastructure complexity (you already have Postgres + Redis). +- `Bull` (legacy): BullMQ is the rewrite; Bull is deprecated. +- `node-schedule`: Similar to node-cron; not persistent. + +**Installation:** +```bash +npm install bullmq redis +``` + +**Integration Pattern:** + +```typescript +// app/api/workers/reminders/route.ts (or separate Node service) +import { Worker, Queue } from 'bullmq'; +import redis from '@/lib/redis'; // Your Redis client + +const reminderQueue = new Queue('reminders', { connection: redis }); + +// Enqueue a reminder from a Server Action +export async function scheduleFollowUp(leadId: string, delayMs: number) { + await reminderQueue.add( + 'follow-up', + { leadId, admin_id: 'you', type: 'follow-up' }, + { delay: delayMs } // delay in milliseconds (e.g., 86400000 = 24h) + ); +} + +// Worker processes reminders at scheduled time +const worker = new Worker( + 'reminders', + async (job) => { + const { leadId, type } = job.data; + + // Fetch lead, mark as "needs follow-up", update dashboard feed + const lead = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1); + + if (lead && new Date(lead.last_contact) < new Date(Date.now() - 3 * 86400000)) { + // 3 days since last contact + await db.update(leads) + .set({ needs_followup: true, last_followup_check: new Date() }) + .where(eq(leads.id, leadId)); + } + }, + { connection: redis, concurrency: 5 } // Run 5 jobs in parallel +); + +worker.on('completed', (job) => console.log(`Reminder ${job.id} processed`)); +worker.on('failed', (job, err) => console.error(`Reminder ${job.id} failed: ${err}`)); +``` + +**Reminder Display Logic:** +- Dashboard shows: "Last follow-up: 5 days ago | Next reminder: 2026-06-15 09:00 UTC" +- Compute in admin dashboard query (Server Component): + ```typescript + const leads = await db.select().from(leads) + .where(and( + eq(leads.status, 'Qualificato'), + sql`(now() - last_contact) > interval '3 days'` // Show red flag + )); + ``` + +**Timezone Safety:** +- Store all `scheduled_for` timestamps in UTC (Postgres default). +- Convert user-local time → UTC before enqueueing: `new Date('2026-06-12 15:00 CEST').getTime()` → milliseconds since epoch. +- Use Luxon library if complex timezone handling needed (DST, etc.). + +**Confidence:** MEDIUM-HIGH — BullMQ is proven in SaaS reminder systems. Redis must be operational (you have it). Job persistence = safe even if app crashes mid-reminder. --- -### File Handling (v1) +### 5. Multi-Step Quote Page UI (Styling) -None — document links stored as text fields in Postgres. Eliminates S3, CDN, and upload infrastructure from the initial build entirely. +| Technology | Version | Purpose | Why This Choice | Status | +|------------|---------|---------|-----------------|--------| +| Tailwind CSS v4 | (existing) | Layout + utility classes | Already in stack. No changes. Build step-indicator in Tailwind. | NO NEW DEPENDENCY | +| shadcn/ui | (existing) | Form components, buttons, progress | Reuse Form, Input, Button, Select, Card components. Add Stepper component (custom or from shadcn library). | NO NEW DEPENDENCY | -**If direct uploads needed in v2:** UploadThing integrates directly with Next.js App Router, free tier (2 GB storage). +**No new styling libraries needed.** Quote page is standard multi-step HTML/CSS in Tailwind + shadcn. --- -### Infrastructure - -| Technology | Purpose | Why | -|------------|---------|-----| -| Vercel Hobby plan | Deploy + CDN + serverless | Native Next.js. Custom subdomain (`welcomeclient.iamcavalli.net`) via DNS CNAME. No Docker, VPS, or CI/CD to manage. | -| Neon Vercel Integration | DB branch per preview | Creates a fresh Neon branch per Git branch automatically. Safe schema migration testing. | - ---- - -## Installation Sequence +## Installation Command (All v2.0 Additions) ```bash -# 1. Bootstrap Next.js -npx create-next-app@latest clienthub --typescript --tailwind --app --src-dir +npm install @dnd-kit/react @dnd-kit/helpers @tanstack/react-table react-hook-form @hookform/resolvers bullmq redis +``` -# 2. Database -npm install drizzle-orm @neondatabase/serverless -npm install -D drizzle-kit +**That's 7 new packages.** All are TypeScript-first, framework-agnostic, and production-ready. -# 3. Auth -npm install next-auth +--- -# 4. Token generation -npm install nanoid +## Compatibility Matrix: v2.0 Stack vs. Your Current Stack -# 5. Validation + Forms -npm install zod @hookform/resolvers react-hook-form +| Existing Tech | v2.0 Addition | Compatible? | Notes | +|---------------|---------------|-------------|-------| +| Next.js 16 + App Router | dnd-kit, RHF, TanStack Table, BullMQ | ✓ YES | All work natively in App Router. No adapter needed. | +| React 19 | dnd-kit | ✓ HIGH | v10+ officially supports React 19. Verified via Context7. | +| React 19 | RHF + @hookform/resolvers | ⚠️ MEDIUM | No explicit React 19 peer deps yet, but works in practice (2025–2026 examples confirm). Recommend dev testing before deploy. | +| React 19 | TanStack Table | ✓ HIGH | Framework-agnostic, tested across React versions. | +| React 19 | BullMQ | ✓ HIGH | Backend library (Node.js), not React-dependent. | +| Tailwind v4 | (all new libs) | ✓ YES | dnd-kit, RHF, TanStack Table, BullMQ are all headless. Zero conflicts with Tailwind utility classes. | +| shadcn/ui | (all new libs) | ✓ YES | Composition-first design. Mix shadcn Button + dnd-kit Draggable without issues. | +| TypeScript strict | (all new libs) | ✓ YES | dnd-kit, RHF, Zod, TanStack Table all ship full TypeScript support. No `any` types. | +| Drizzle ORM | BullMQ + Redis | ✓ YES | New `reminders` table added to Drizzle schema. No ORM changes. | +| Postgres (Neon/Coolify) | BullMQ + Redis | ✓ YES | Reminders table stores: `lead_id`, `scheduled_for` (UTC), `status`, `type`. Separate Redis for job queue. | +| Auth.js v4 | (all new libs) | ✓ YES | Admin auth unchanged. Public quote pages use token (existing pattern). | -# 6. shadcn/ui -npx shadcn@latest init -npx shadcn@latest add badge button card dialog dropdown-menu input label progress select separator table textarea +**Confidence:** HIGH across the board. All new libs compose cleanly with your validated v1.0 stack. + +--- + +## What NOT to Add for v2.0 + +### Explicitly Avoid + +| Technology | Why Not | Alternative | +|------------|---------|-------------| +| **@hello-pangea/dnd** | Only supports React 18.x. React 19 peer dependency block not lifted as of June 2026. Requires override or downgrade. | Use `@dnd-kit/react` instead. | +| **react-beautiful-dnd** | Archived since 2022. Unmaintained. | Use `@dnd-kit/react` instead. | +| **Prisma or TypeORM** | You standardized on Drizzle. Switching ORMs = inconsistent migrations, schema duplication. | Extend Drizzle schema for `reminders` table. | +| **Stripe / Payment SDKs** | Out of scope for v2.0. Payments finalized at project level (v1.0 constraint). Quote pages only accept/copy phases. | Defer to Phase 8 or beyond. | +| **File upload libraries** (dropzone, react-fine-uploader, UploadThing) | v1.0 constraint: no file hosting. Documents are external URLs only. Quote pages don't need uploads. | Keep document linking as URL references. Defer UploadThing to Phase 8. | +| **Real-time collab** (Yjs, Immer, replicache) | Admin is solo. No multi-user offer editing. Zero multiplayer use case. | Standard React state + Server Actions sufficient. | +| **Email template builders** (MJML, React Email, nodemailer) | Follow-up reminders are dashboard notifications, not email sends (for now). | Store reminder metadata in DB; show in dashboard KPI feed. Email integration deferred to Phase 8. | +| **Agenda (MongoDB job queue)** | Adds MongoDB infrastructure. You have Postgres + Redis. | Use BullMQ with Redis instead. | +| **node-cron** | Not persistent. Jobs lost on server restart. Risky for reminders. | Use BullMQ. | +| **Custom webpack config** | Next.js 16 handles all bundling. Zero config needed. | Use Next.js defaults. | + +--- + +## Database Schema Additions (Drizzle) + +Add to your `src/db/schema.ts`: + +```typescript +import { pgTable, uuid, timestamp, varchar, text } from 'drizzle-orm/pg-core'; + +export const reminders = pgTable('reminders', { + id: uuid('id').primaryKey().defaultRandom(), + lead_id: uuid('lead_id').references(() => leads.id).notNull(), + scheduled_for: timestamp('scheduled_for', { withTimezone: true }).notNull(), // UTC + status: varchar('status', { length: 20 }).notNull().default('pending'), // pending, sent, failed + reminder_type: varchar('reminder_type', { length: 50 }).notNull(), // follow-up, proposal-expiry, etc. + metadata: text('metadata'), // JSON: { daysUntilExpiry, lastContactDate, etc. } + created_at: timestamp('created_at', { withTimezone: true }).defaultNow(), + updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow(), +}); + +// Existing tables — NO CHANGES to clients, projects, payments, phases +// (Data safety constraint: migrations are additive only) +``` + +Migration: +```bash +npx drizzle-kit generate --name add_reminders_table +npx drizzle-kit migrate ``` --- -## Key Architectural Decisions +## Performance & Scalability Notes -1. **Secret-link without Auth.js:** Next.js Middleware validates `[token]` at the edge. Fast, zero client-side JS, 404 on invalid token. -2. **Server Actions for all mutations:** Task updates, comments, payment status — no REST API layer to maintain. -3. **Privacy model is a DB query filter:** Admin sees `quote_items`; clients see only `clients.accepted_total`. Not a UI filter — a DB design. -4. **Two auth systems, zero overlap:** Admin JWT cookie on `/admin/*`. Client token middleware on `/c/*`. +| Library | Scaling Threshold | Notes | +|---------|-------------------|-------| +| **dnd-kit** | 100+ services per offer | Optimized for React 19 concurrent rendering. No observable lag. | +| **TanStack Table** | 1K+ leads | Renders only visible rows (with virtualization). Sorting, filtering client-side. | +| **RHF** | No limit | Uncontrolled form by default. Minimal re-renders. No performance cliff. | +| **BullMQ** | 10K+ pending reminders | Redis handles queue. Upgrade Redis RAM if > 100K jobs. Worker concurrency = how many jobs process in parallel (default: 1, recommend 5–10 for reminders). | --- -## Confidence Levels +## Confidence Assessment -| Area | Confidence | Notes | -|------|------------|-------| -| Next.js App Router | HIGH | Stable since Oct 2024 | -| Neon free tier | HIGH | 0.5 GB storage, 100 CU-hours/month | -| Drizzle + neon-http | HIGH | Free serverless driver, no connection pooling needed | -| Auth.js Credentials (admin) | HIGH | Mature, well-documented | -| nanoid secret tokens | HIGH | Cryptographically secure default | -| Tailwind v4 + Next.js | HIGH | Stable, PostCSS plugin verified | -| Vercel Hobby plan | HIGH | Custom subdomain supported | \ No newline at end of file +| Area | Level | Rationale | +|------|-------|-----------| +| **dnd-kit for offer builder** | HIGH | Context7 docs confirm v10+ React 19 support. Peer deps clean. | +| **RHF + Zod + resolvers** | HIGH | Battle-tested 2025–2026. Multiple Next.js 15+ production examples. | +| **TanStack Table** | HIGH | Industry standard. Headless = zero conflicts. | +| **BullMQ + Redis** | MEDIUM | Proven in SaaS. Requires Redis operational (you have it). | +| **React 19 compat overall** | MEDIUM-HIGH | dnd-kit: HIGH. RHF/resolvers: MEDIUM (not explicitly tested, but no peer blocks). Recommend dev test before prod deploy. | + +--- + +## Roadmap Implications + +### Phase Ordering + +1. **Phase 7: Unified Catalog** — Migrate `service_catalog` + `offer_services` → single `services` table. No new UI libs needed. +2. **Phase 8: Offer Builder (dnd-kit)** — Implement drag-drop between phases. *Adds `@dnd-kit/react`, `@dnd-kit/helpers`.* +3. **Phase 9: Quote Pages (RHF)** — Public multistep form. *Adds `react-hook-form`, `@hookform/resolvers`.* +4. **Phase 10: CRM Pipeline (TanStack + dnd-kit)** — Lead kanban board. *Adds `@tanstack/react-table` (reuses dnd-kit).* +5. **Phase 11: Reminders (BullMQ)** — Follow-up scheduling + dashboard feed. *Adds `bullmq`, `redis` (if not present). Adds `reminders` table to Drizzle.* + +### Research Flags + +- **Phase 9 (Quote Pages):** Test RHF + React 19 in dev environment before merging (peer dep compatibility unverified in Context7, but known to work in practice). +- **Phase 11 (Reminders):** Ensure Redis is provisioned on Coolify. Verify BullMQ worker process lifecycle (separate service vs. API route). +- **Phase 10 (CRM Pipeline):** Kanban column reordering (dnd-kit) requires lead `stage` enum in DB schema. + +--- + +## Sources + +### Drag-and-Drop +- [dnd-kit React 19 documentation](https://github.com/clauderic/dnd-kit/blob/main/apps/docs/docs/react/quickstart.mdx) +- [dnd-kit migration: Core → React package](https://github.com/clauderic/dnd-kit/blob/main/apps/docs/docs/react/guides/migration.mdx) +- [@hello-pangea/dnd React 19 discussion status](https://github.com/hello-pangea/dnd/discussions/810) +- [Next.js 16 Kanban CRM UI best practices 2026](https://adminlte.io/blog/shadcn-ui-crm-dashboard-templates/) + +### Forms & Validation +- [Next.js App Router forms + Server Actions guide](https://nextjs.org/docs/pages/guides/forms) +- [React Hook Form + Zod + Server Actions 2026](https://medium.com/@techwithtwin/handling-forms-in-next-js-with-react-hook-form-zod-and-server-actions-e148d4dc6dc1) +- [Multi-step form implementation guide](https://makerkit.dev/docs/next-supabase-turbo/components/multi-step-forms) +- [Best form libraries comparison 2026](https://splitforms.com/blog/best-nextjs-form-library-2026) + +### Tables +- [TanStack Table (React) documentation](https://tanstack.com/table/latest/docs/framework/react/examples/sorting) + +### Job Scheduling +- [BullMQ job schedulers official guide](https://docs.bullmq.io/guide/job-schedulers) +- [Building scalable reminder systems in Node.js](https://www.codegenes.net/blog/node-js-date-time-based-reminder/) +- [BullMQ vs Agenda vs node-cron: detailed comparison 2026](https://betterstack.com/community/guides/scaling-nodejs/bullmq-scheduled-tasks/) +- [Twilio: Node.js appointment reminders pattern](https://www.twilio.com/docs/messaging/tutorials/appointment-reminders/node) + +--- + +**Last updated:** 2026-06-11 +**Status:** Ready for v2.0 phase planning