89adf7a1d4
UI-SPEC approved (5/6 PASS, 1 non-blocking flag on focal point). Locks reuse of Phase 11 primitives (EditableCell, OptionSelect, OptionMultiSelect) for the leads table, plus scope for the four CRM bug fixes (i18n, type-safety, dead code). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
443 lines
21 KiB
Markdown
443 lines
21 KiB
Markdown
---
|
||
phase: 14
|
||
slug: crm-attio-style-fix
|
||
status: approved
|
||
shadcn_initialized: true
|
||
preset: "default (neutral base color)"
|
||
created: "2026-06-13"
|
||
---
|
||
|
||
# Phase 14 — UI Design Contract
|
||
|
||
> Visual and interaction contract for CRM table redesign (Attio-style) and bug fixes. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||
|
||
---
|
||
|
||
## Design System
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Tool | shadcn/ui |
|
||
| Preset | default (neutral, baseColor: neutral) |
|
||
| Component library | radix-ui (via shadcn) |
|
||
| Icon library | lucide-react |
|
||
| Font | Geist Sans (system fallback) |
|
||
|
||
**Source:** `components.json` and `src/app/globals.css` (Phase 11, confirmed 2026-06-13)
|
||
|
||
---
|
||
|
||
## Spacing Scale
|
||
|
||
Declared values (multiples of 4):
|
||
|
||
| Token | Value | Usage |
|
||
|-------|-------|-------|
|
||
| xs | 4px | Icon gaps, inline padding |
|
||
| sm | 8px | Compact element spacing |
|
||
| md | 16px | Default element spacing (py-2 px-3 ≈ 8px/12px) |
|
||
| lg | 24px | Section padding |
|
||
| xl | 32px | Layout gaps |
|
||
| 2xl | 48px | Major section breaks |
|
||
| 3xl | 64px | Page-level spacing |
|
||
|
||
**Exceptions:** None — standard 4px-multiple scale
|
||
|
||
**Implementation:** Tailwind utility classes (py-2 px-3 for table cells, gap-1.5 for dropdown spacing, etc.). Reuse `EditableCell`, `OptionSelect`, `OptionMultiSelect` internal spacing without modification.
|
||
|
||
---
|
||
|
||
## Typography
|
||
|
||
| Role | Size | Weight | Line Height |
|
||
|------|------|--------|-------------|
|
||
| Body | 16px (default) | 400 (regular) | 1.6 |
|
||
| Label / Small | 14px | 400 (regular) | 1.5 |
|
||
| XSmall / Badge | 12px | 500 (medium, via `font-medium`) | 1.4 |
|
||
| Heading (page) | 20px | 600 (semibold, via `font-semibold`) | 1.2 |
|
||
|
||
**Source:** Tailwind defaults + Phase 11 component internals (EditableCell: `text-sm`, OptionSelect/OptionMultiSelect: `text-xs badge, text-sm button`)
|
||
|
||
**Details:**
|
||
- Body: 16px, weight 400, line-height 1.6 (per `globals.css` body)
|
||
- Table cell labels (lead name, email, status): `text-sm` = 14px, weight 400
|
||
- Badge labels (status stage, tags): `text-xs` = 12px, weight 500 (`font-medium`)
|
||
- Placeholder text: `text-[#71717a]` (muted-foreground)
|
||
- Error messages: `text-xs text-red-600`
|
||
|
||
---
|
||
|
||
## Color
|
||
|
||
| Role | Value | Usage |
|
||
|------|-------|-------|
|
||
| Dominant (60%) | #ffffff (white) | Page background, card backgrounds |
|
||
| Secondary (30%) | #f4f4f5 (neutral-100) | Hover states, subtle section backgrounds, table row hover |
|
||
| Accent (10%) | #1A463C (primary, dark green) | Button text, icon colors, ring/focus, checkbox accent |
|
||
| Destructive | #ef4444 (red) | Error messages, delete/dangerous action badges |
|
||
|
||
**Semantic colors (for lead status badges):**
|
||
- Contacted: blue (bg-blue-100 text-blue-800)
|
||
- Qualified: purple (bg-purple-100 text-purple-800)
|
||
- Proposal Sent: amber (bg-amber-100 text-amber-800)
|
||
- Negotiating: orange (bg-orange-100 text-orange-800)
|
||
- Won: green (bg-green-100 text-green-800)
|
||
- Lost: red (bg-red-100 text-red-800)
|
||
|
||
**Accent reserved for:**
|
||
- Form focus rings (input/textarea `ring-primary`)
|
||
- Checkbox `accent-[#1A463C]`
|
||
- Icon colors in OptionSelect/OptionMultiSelect (checkmarks, pencil, plus, X)
|
||
- Primary button text
|
||
- Selected state indicators (table row actions)
|
||
|
||
**Notes:**
|
||
- Table row hover: `hover:bg-[#f9f9f9]` (Phase 11 ServiceTable pattern)
|
||
- Border color: `border-[#e5e7eb]`
|
||
- Input ring (unfocused): `ring-1 ring-primary` (#1A463C, 1px border)
|
||
- Error ring: `ring-2 ring-red-500` (2px on error state)
|
||
- OptionSelect/OptionMultiSelect dropdown: white background, 1.5px shadow, 1px border
|
||
|
||
---
|
||
|
||
## Copywriting Contract
|
||
|
||
| Element | Copy | Context |
|
||
|---------|------|---------|
|
||
| Primary CTA | "Salva" (implicit, on Enter key) | Inline cell edit commit on Enter or blur |
|
||
| CTA — Add Lead | "Nuovo Lead" | CreateLeadModal button (unchanged from Phase 10) |
|
||
| CTA — Tag Create | "Crea «{tag_name}»" | OptionMultiSelect dropdown, create-on-type UX |
|
||
| CTA — Status Create | N/A (fixed enum) | Status uses OptionSelect but with fixed LEAD_STAGES, no create-on-the-fly |
|
||
| Empty state heading | "Nessun lead trovato" | LeadTable when leads.length === 0 |
|
||
| Empty state body | (no secondary message) | Single-line empty state, matching ServiceTable pattern |
|
||
| Required field error | "Campo richiesto" | EditableCell when required field is empty on blur |
|
||
| Invalid status | "Stato non valido" | updateLeadField server action validation |
|
||
| Activity log | "Attività registrata" (feedback on LogActivityModal submit) | Unchanged from Phase 10 |
|
||
| FollowUpWidget — (CRM-10, i18n) | "Richiedi Follow-up" (heading), "lead/lead" (plural), "da contattare" (to contact), "Contatta" (button), "Vedi Tutto" (view all), "Tutti i lead sono aggiornati!" (no follow-ups) | Dashboard widget, fully Italian |
|
||
|
||
**Destructive actions:**
|
||
- None explicit in Phase 14 scope (lead deletion already exists, not modified by CRM-08..12)
|
||
- If a "delete lead" row action is added later, copy: "Elimina lead: Sei sicuro? Questa azione non può essere annullata."
|
||
|
||
---
|
||
|
||
## Component Inventory
|
||
|
||
### Reused from Phase 11 (no new imports needed)
|
||
|
||
| Component | Path | Purpose in Phase 14 |
|
||
|-----------|------|-------------------|
|
||
| `EditableCell` | `src/components/ui/editable-cell.tsx` | Inline edit for lead name, email, phone, company, next_action |
|
||
| `OptionSelect` | `src/components/ui/option-select.tsx` | Lead status dropdown (fixed enum: LEAD_STAGES, 6 values, no create-on-the-fly) |
|
||
| `OptionMultiSelect` | `src/components/ui/option-multi-select.tsx` | Lead tags multi-select (CRM-09: create-on-the-fly enabled) |
|
||
| `getOptionColor` | `src/components/ui/option-colors.ts` | Badge color generation for tags (reused directly by OptionMultiSelect) |
|
||
| `Badge` | `src/components/ui/badge` | Display lead status & tags badges |
|
||
| `Input` | `src/components/ui/input` | Inline edit inputs, search box in new LeadsSearch component |
|
||
| `Button` | `src/components/ui/button` | Detail/action buttons (unchanged from Phase 10) |
|
||
|
||
### To Create / Modify
|
||
|
||
| Component | Path | Change | Scope |
|
||
|-----------|------|--------|-------|
|
||
| `LeadTable` | `src/components/admin/leads/LeadTable.tsx` | REWRITE — replace shadcn `<Table>` wrapper with raw `<table>`, add EditableCell + OptionSelect/OptionMultiSelect per row | CRM-08, CRM-09 |
|
||
| `LeadsSearch` | `src/app/admin/leads/LeadsSearch.tsx` | NEW — client-side search component (useMemo filter on name/email/company/status/tags), mirrors CatalogSearch.tsx | CRM-08 UX (instant filter) |
|
||
| `FollowUpWidget` | `src/components/admin/dashboard/FollowUpWidget.tsx` | FIX (CRM-10) — translate 6 hardcoded English strings to Italian | CRM-10 |
|
||
| `LeadForm` | `src/components/admin/leads/LeadForm.tsx` | FIX (CRM-11) — remove `useForm<any>`, use Zod-inferred CreateLeadInput/UpdateLeadInput types, remove `as any` casts | CRM-11 |
|
||
| `SendQuoteModal` | `src/components/admin/leads/SendQuoteModal.tsx` | FIX (CRM-12) — remove unreachable `if (tab === "new")` branch from onSubmit | CRM-12 |
|
||
|
||
---
|
||
|
||
## Table Layout — LeadTable (CRM-08, CRM-09)
|
||
|
||
### Visual Pattern (from Phase 11 ServiceTable)
|
||
|
||
```
|
||
┌─ Lead Table ─────────────────────────────────────────────────────────────────┐
|
||
│ Nome │ Email │ Stato │ Prossima Azione │ Tag │
|
||
├───────────────────┼─────────────────┼───────────┼───────────────────┼────────┤
|
||
│ [EditableCell] │ [EditableCell] │ [Status] │ [EditableCell] │ [Tags] │
|
||
│ hover: bg-#f9f9f9 │ │ │ │ │
|
||
│ border-b #e5e7eb │ │ │ │ │
|
||
├───────────────────┼─────────────────┼───────────┼───────────────────┼────────┤
|
||
│ ... │ ... │ ... │ ... │ ... │
|
||
└───────────────────┴─────────────────┴───────────┴───────────────────┴────────┘
|
||
```
|
||
|
||
### Column Structure
|
||
|
||
| Column | Type | Editable | Required | Cell Component | Min-Width | Notes |
|
||
|--------|------|----------|----------|----------------|-----------|-------|
|
||
| Nome | text | Yes | Yes | EditableCell(type="text") | 160px | Lead name, bold, linked to detail page |
|
||
| Email | text | Yes | No | EditableCell(type="text") | 160px | Nullable, show "—" if empty |
|
||
| Telefono | text | Yes | No | EditableCell(type="text") | 140px | Nullable, show "—" if empty |
|
||
| Azienda | text | Yes | No | EditableCell(type="text") | 140px | Nullable, show "—" if empty |
|
||
| Stato | enum | Yes | Yes | OptionSelect(options=LEAD_STAGES, no create) | 120px | 6 fixed values: contacted, qualified, proposal_sent, negotiating, won, lost |
|
||
| Prossima Azione | text | Yes | No | EditableCell(type="text") | 180px | Nullable, show "—" if empty |
|
||
| Tag | multi-select | Yes | No | OptionMultiSelect(create-on-the-fly) | 180px | CRM-09: lead tags, polymorphic table (entity_type="leads") |
|
||
| Azioni | link | No | N/A | Link → `/admin/leads/[id]` | 80px | "Dettagli" button, unchanged from Phase 10 |
|
||
|
||
### Row States
|
||
|
||
- **Normal:** white background, border-b #e5e7eb, text #1a1a1a
|
||
- **Hover:** bg-#f9f9f9, smooth 150ms transition
|
||
- **Cell edit (active):** EditableCell in input mode — ring-1 ring-primary, h-8 for text inputs
|
||
- **Cell error:** ring-2 ring-red-500, error message text-xs text-red-600 below cell
|
||
- **Row with error:** shows error row spanning all columns, text-xs text-red-600
|
||
|
||
### Search/Filter (CRM-08 UX — "Attio-style instant")
|
||
|
||
- New component: `LeadsSearch` (client-side wrapper)
|
||
- Input: search box at top of page (before table)
|
||
- Behavior: `useMemo` filter on keystroke, no server round-trip
|
||
- Filter fields: name, email, company, status label, tag names (all case-insensitive, includes-search)
|
||
- Display: filtered leads in table below; if no results, empty state "Nessun lead trovato"
|
||
- Placeholder: "Cerca lead..." (consistent with CatalogSearch style)
|
||
|
||
---
|
||
|
||
## Bug Fixes
|
||
|
||
### CRM-10: FollowUpWidget Internationalization
|
||
|
||
**Current state:** 6 hardcoded English strings in JSX.
|
||
|
||
**Strings to translate:**
|
||
1. "Require Follow-up" → "Richiedi Follow-up"
|
||
2. "lead" / "leads" (noun, "X lead to contact") → "lead" / "lead" (Italian stays same, but handle plural: "X lead da contattare" / "X lead da contattare")
|
||
3. "to contact" → "da contattare"
|
||
4. "Contact" (button label) → "Contatta"
|
||
5. "View All" → "Vedi Tutto"
|
||
6. "All leads are up to date!" → "Tutti i lead sono aggiornati!"
|
||
|
||
**Style:** Tone matches existing `LeadDetail.tsx`/`LeadTable.tsx` (semi-formal, second-person friendly).
|
||
|
||
**Location:** `src/components/admin/dashboard/FollowUpWidget.tsx` JSX only (no data-layer strings confirmed).
|
||
|
||
### CRM-11: LeadForm Type Safety (react-hook-form)
|
||
|
||
**Current pattern:** `useForm<any>()`, `field.value as any`, `data: any` in onSubmit.
|
||
|
||
**Fix pattern:**
|
||
- Import `CreateLeadInput` / `UpdateLeadInput` from `src/lib/lead-validators.ts` (Zod-inferred types)
|
||
- Change `useForm<any>()` → `useForm<CreateLeadInput>()` (or UpdateLeadInput for edit modal)
|
||
- Replace `as any` casts with type-safe narrowing at schema boundary (e.g., `lead.status as CreateLeadInput["status"]`)
|
||
- Remove blanket `data: any` in `onSubmit` callback signature
|
||
- Nullable fields: use `field.value || ""` pattern (already established elsewhere in the app)
|
||
|
||
**No schema changes required** — existing `createLeadSchema`/`updateLeadSchema` are correct; this is purely a type-safety fix in the component.
|
||
|
||
### CRM-12: SendQuoteModal Unreachable Code
|
||
|
||
**Current problem:** `if (tab === "new")` branch inside `onSubmit` (form submit handler) is unreachable because the "new" tab has its own standalone button with `onClick` handler that does a redirect, never submitting the form.
|
||
|
||
**Fix:**
|
||
- Remove the unreachable `if (tab === "new")` branch from `onSubmit` handler
|
||
- Optionally simplify `onSubmit` to only handle the "existing quote" path (tab === "existing")
|
||
- Confirm `generate_new` field in schema/defaultValues is also dead code (currently always false, unused in action)
|
||
|
||
**Scope:** Code cleanup only; no UX change to "new" tab button (still does redirect as-is).
|
||
|
||
---
|
||
|
||
## Server Actions (Backend Contract)
|
||
|
||
### New Actions Required (CRM-08, CRM-09)
|
||
|
||
**File:** `src/app/admin/leads/actions.ts` (extend existing)
|
||
|
||
```typescript
|
||
// Pattern 1: Field update with per-field validation
|
||
export async function updateLeadField(
|
||
leadId: string,
|
||
fieldName: EditableField,
|
||
value: string
|
||
): Promise<void> {
|
||
// Validates fieldName against EDITABLE_FIELDS allowlist
|
||
// Applies field-specific validation (name required, status in LEAD_STAGES, etc.)
|
||
// Calls db.update() via Drizzle ORM
|
||
// Calls revalidatePath("/admin/leads") and revalidatePath(`/admin/leads/${leadId}`)
|
||
}
|
||
|
||
// Pattern 2: Tag CRUD (polymorphic tags table)
|
||
export async function addLeadTag(leadId: string, value: string): Promise<void>
|
||
export async function removeLeadTag(leadId: string, value: string): Promise<void>
|
||
export async function renameLeadTag(oldValue: string, newValue: string): Promise<void>
|
||
// All three call requireAdmin() at the top
|
||
// Use tags table with entity_type="leads", entity_id=leadId
|
||
// Call revalidatePath("/admin/leads") after each mutation
|
||
```
|
||
|
||
**Auth guard:** All new actions must include `await requireAdmin()` check (per Phase 11 convention in `catalog/actions.ts`).
|
||
|
||
### Query Functions (CRM-08 data fetch)
|
||
|
||
**File:** `src/lib/admin-queries.ts` (extend) or new `src/lib/lead-service.ts`
|
||
|
||
```typescript
|
||
export type LeadWithTags = Lead & { tags: string[] };
|
||
export type LeadFieldOptions = { status: string[]; tags: string[] };
|
||
|
||
export async function getLeadsWithTags(): Promise<LeadWithTags[]>
|
||
// Left-join leads + tags table (entity_type="leads")
|
||
// Map-reduce to attach tags array per lead
|
||
// Return sorted by updated_at DESC, tags ASC
|
||
|
||
export async function getLeadFieldOptions(): Promise<LeadFieldOptions>
|
||
// Return status: LEAD_STAGES (fixed enum)
|
||
// Return tags: distinct tag names for entity_type="leads", sorted by locale "it"
|
||
```
|
||
|
||
---
|
||
|
||
## Interaction Flows
|
||
|
||
### Flow 1: Inline Edit Lead Field (CRM-08)
|
||
|
||
```
|
||
User clicks cell
|
||
↓
|
||
EditableCell → input mode (focus + select text)
|
||
↓
|
||
User types + presses Enter (or loses focus)
|
||
↓
|
||
EditableCell.commit() → calls onSave callback
|
||
↓
|
||
Callback runs server action updateLeadField(leadId, fieldName, value)
|
||
↓
|
||
Server action validates, updates DB, revalidates paths
|
||
↓
|
||
Router.refresh() re-renders page with new data
|
||
↓
|
||
EditableCell exits edit mode, shows new value
|
||
```
|
||
|
||
**Error handling:** If server action throws, error message displays below cell (text-xs text-red-600). User can retry by clicking cell again.
|
||
|
||
### Flow 2: Multi-Select Lead Tags (CRM-09)
|
||
|
||
```
|
||
User clicks tag cell
|
||
↓
|
||
OptionMultiSelect → dropdown opens, shows pill list + search input
|
||
↓
|
||
User types tag name (or selects existing)
|
||
↓
|
||
If new: shows "Crea «{name}»" button
|
||
↓
|
||
User presses Enter or clicks button
|
||
↓
|
||
Calls onAdd callback → server action addLeadTag(leadId, tagName)
|
||
↓
|
||
Server action inserts into tags table (entity_type="leads")
|
||
↓
|
||
Router.refresh() + revalidate paths
|
||
↓
|
||
OptionMultiSelect pill list updates with new tag (+ remove button)
|
||
```
|
||
|
||
**Remove tag:** User clicks × button on pill → calls onRemove callback → removeLeadTag action.
|
||
|
||
**Rename tag:** User clicks pencil icon on option → inline input → commitRename → renameLeadTag action (propagates to all leads with this tag).
|
||
|
||
### Flow 3: Client-Side Search (CRM-08 UX)
|
||
|
||
```
|
||
User types in search box (LeadsSearch component)
|
||
↓
|
||
useState(query) triggers useMemo filter
|
||
↓
|
||
Filter includes: name, email, company, status, tags (all case-insensitive)
|
||
↓
|
||
Filtered leads array updates
|
||
↓
|
||
LeadTable re-renders with filtered rows
|
||
↓
|
||
No server call, instant response
|
||
```
|
||
|
||
**Clear search:** User clears input → all leads shown again.
|
||
|
||
**Empty results:** LeadTable displays "Nessun lead trovato" empty state.
|
||
|
||
---
|
||
|
||
## Registry Safety
|
||
|
||
| Registry | Blocks Used | Status |
|
||
|----------|-------------|--------|
|
||
| shadcn official | Badge, Input, Button, Table (deprecated in new LeadTable), Textarea, Table import removal | no vetting required |
|
||
| third-party | none | N/A |
|
||
|
||
**Notes:**
|
||
- Phase 14 removes the shadcn `Table` wrapper from LeadTable (switches to raw `<table>` for consistency with Phase 11 ServiceTable pattern)
|
||
- No new third-party registries introduced
|
||
- All components reused from Phase 11 or existing shadcn official library
|
||
|
||
---
|
||
|
||
## Checker Sign-Off
|
||
|
||
- [x] Dimension 1 Copywriting: PASS
|
||
- [x] Dimension 2 Visuals: FLAG — no explicit focal point declared for the LeadsSearch + LeadTable screen (non-blocking; recommend "Primary focal point: search input bar at top; secondary: lead name column (bold, linked to detail)")
|
||
- [x] Dimension 3 Color: PASS
|
||
- [x] Dimension 4 Typography: PASS
|
||
- [x] Dimension 5 Spacing: PASS
|
||
- [x] Dimension 6 Registry Safety: PASS
|
||
|
||
**Approval:** APPROVED (2026-06-14) — 5/6 PASS, 1 non-blocking FLAG (Visuals/focal point)
|
||
|
||
---
|
||
|
||
## Design Decisions & Rationale
|
||
|
||
### Decision 1: Status Field — OptionSelect with Fixed LEAD_STAGES (not extensible)
|
||
|
||
**What:** Lead `status` uses `OptionSelect` component but with `options={LEAD_STAGES}` (6 fixed values: contacted, qualified, proposal_sent, negotiating, won, lost) and **no create-on-the-fly** UX.
|
||
|
||
**Why:** `status` is a closed enum, not an open-ended Notion-style property like tags. Allowing users to create arbitrary status values would break pipeline logic, STAGE_COLOR lookups, and LEAD_STAGES-based filters elsewhere (LeadDetail.tsx, FollowUpWidget, queries). OptionSelect is reused for interaction consistency with the tags column (click-to-open dropdown, keyboard nav, rename support), but the input validation (Pattern 2) enforces that only LEAD_STAGES values are accepted server-side, regardless of client-side UI.
|
||
|
||
**Semantic colors preserved:** Badge colors for status still use STAGE_COLOR map (blue/purple/amber/orange/green/red), not `getOptionColor()`'s hash-based pastels, to maintain pipeline-stage meaning.
|
||
|
||
### Decision 2: LeadTable Layout — Raw `<table>` not shadcn `<Table>` Wrapper
|
||
|
||
**What:** Phase 14 LeadTable uses raw HTML `<table>/<tr>/<td>` elements with Tailwind classes, exactly like Phase 11 ServiceTable.
|
||
|
||
**Why:**
|
||
1. **Visual consistency:** Phase 11 established the "database-view" pattern (ServiceTable) as the standard for inline-editable Attio-style tables; replicating it for leads ensures visual/interaction coherence.
|
||
2. **Component reuse:** EditableCell/OptionSelect/OptionMultiSelect are already styled for raw-table cells (py-2 px-3, min-w-*, inline spacing); wrapping them in shadcn `<Table>` wrapper components adds unnecessary nesting.
|
||
3. **Simplicity:** Raw table is easier to customize row-by-row (per-row error messages, inline edit state transitions, per-row hover) than the shadcn wrapper's structured slots.
|
||
|
||
### Decision 3: No Lead Quick-Add Row (CRM-08 scope)
|
||
|
||
**What:** Phase 14 does NOT include a `QuickAddRow` equivalent for leads (Phase 11 added it for services).
|
||
|
||
**Why:** CRM-08 requirement says "inline editing dei campi principali" (existing leads) + CRM-09 says "assegna tag" (existing leads). Neither mentions lead *creation* UX. The existing `CreateLeadModal` is the lead-creation entry point and remains unchanged. Adding a quick-add row would be a nice-to-have (Attio/Pipedrive parity), but it is explicitly **out of scope** for Phase 14 per CRM-08..12.
|
||
|
||
**If user asks for it:** Flag as a follow-up enhancement, separate from Phase 14 planning.
|
||
|
||
### Decision 4: LeadDetail.tsx Tags Display (Claude's Discretion)
|
||
|
||
**What:** Whether `LeadDetail.tsx` (the `/admin/leads/[id]` detail page) should surface the new lead tags in a read-only or editable section.
|
||
|
||
**Current assumption:** Tags are managed and visible in the table only (CRM-09 doesn't explicitly require detail-page display).
|
||
|
||
**Recommendation:** Low-effort addition — if planner agrees, add an `OptionMultiSelect` for tags in LeadDetail's "Profilo" card (same data layer as table). If planner says no, leave LeadDetail as-is.
|
||
|
||
### Decision 5: FollowUpWidget Plural Handling (CRM-10)
|
||
|
||
**What:** The English phrase "X lead to contact" vs. "X leads to contact" — Italian equivalent: "X lead da contattare" (both singular and plural use "lead").
|
||
|
||
**Implementation:** Use template string: `{count} lead da contattare` (always "lead", never "leads" in Italian, since "lead" is a loanword with Italian plural also "lead").
|
||
|
||
---
|
||
|
||
## Open Questions Resolved
|
||
|
||
| Question | Resolution | Rationale |
|
||
|----------|-----------|-----------|
|
||
| Should status use OptionSelect or closed-enum dropdown? | OptionSelect + fixed LEAD_STAGES, no create-on-the-fly | Consistency with table interaction pattern; server-side validation ensures only valid statuses enter DB |
|
||
| Include quick-add row for new leads? | No — out of scope for CRM-08..12 | CRM requirements focus on editing existing leads; CreateLeadModal unchanged |
|
||
| Surface tags in LeadDetail.tsx? | Claude's discretion (recommend yes, low-effort) | Add OptionMultiSelect in detail card if planner approves; keep table as primary interface if not |
|
||
| Retrofit pre-existing lead actions with requireAdmin()? | No — out of scope for Phase 14 | New actions (CRM-08/09) include requireAdmin(); pre-existing gap noted for user awareness, not in this phase's plan |
|
||
|