d115465240
Plan Phase 14 (CRM Attio-style & Fix) into 3 waves: data-layer foundation (query helpers + server actions), LeadTable raw-table rewrite with inline edit/tags, and isolated bug fixes for CRM-10/11/12.
481 lines
25 KiB
Markdown
481 lines
25 KiB
Markdown
---
|
|
phase: 14-crm-attio-style-fix
|
|
plan: 03
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/components/admin/dashboard/FollowUpWidget.tsx
|
|
- src/components/admin/leads/LeadForm.tsx
|
|
- src/components/admin/leads/SendQuoteModal.tsx
|
|
autonomous: true
|
|
requirements: [CRM-10, CRM-11, CRM-12]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "FollowUpWidget shows Italian text only: heading 'Richiedi Follow-up', body 'X lead da contattare' (no English words, no pluralized 's')"
|
|
- "EditLeadModal and CreateLeadModal use useForm<CreateLeadInput>() (not useForm<any>()) — form fields are type-checked against the Zod-inferred lead schema"
|
|
- "SendQuoteModal's onSubmit no longer contains an unreachable 'new' tab branch — the only navigation for the 'new' tab happens via the button's own onClick"
|
|
artifacts:
|
|
- path: "src/components/admin/dashboard/FollowUpWidget.tsx"
|
|
provides: "Italian-only follow-up widget copy"
|
|
contains: "Richiedi Follow-up"
|
|
- path: "src/components/admin/leads/LeadForm.tsx"
|
|
provides: "Typed react-hook-form for lead create/edit"
|
|
contains: "useForm<CreateLeadInput>"
|
|
- path: "src/components/admin/leads/SendQuoteModal.tsx"
|
|
provides: "onSubmit without dead 'new' tab branch"
|
|
contains: "onSubmit"
|
|
key_links:
|
|
- from: "src/components/admin/leads/LeadForm.tsx"
|
|
to: "src/lib/lead-validators.ts"
|
|
via: "import type { CreateLeadInput }"
|
|
pattern: "import.*CreateLeadInput.*lead-validators"
|
|
---
|
|
|
|
<objective>
|
|
Fix three residual CRM bugs identified in RESEARCH.md, each isolated to a single component file with no shared dependencies:
|
|
|
|
1. **CRM-10**: `FollowUpWidget.tsx` has 6 hardcoded English strings — translate to Italian per UI-SPEC.md Decision 5 ("X lead da contattare", no pluralization since Italian "lead" is invariant).
|
|
2. **CRM-11**: `LeadForm.tsx`'s `CreateLeadModal` and `EditLeadModal` both call `useForm<any>()`, defeating react-hook-form's type safety — replace with `useForm<CreateLeadInput>()` using the Zod-inferred type already exported from `lead-validators.ts`, removing the local type redeclaration and narrowing the one unavoidable `status` cast.
|
|
3. **CRM-12**: `SendQuoteModal.tsx`'s `onSubmit` contains a dead `if (tab === "new") {...}` branch that can never execute (the "new" tab's button has its own `onClick` handler and never triggers form submit) — remove the unreachable branch.
|
|
|
|
Purpose: Closes out the three non-UI residual bugs from Phase 14's scope, completing CRM-10/11/12 alongside the Attio-style table redesign (CRM-08/09 in Plans 14-01/14-02).
|
|
|
|
Output: Three independently-fixed components, each type-checked and linted.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/ROADMAP.md
|
|
@.planning/REQUIREMENTS.md
|
|
@.planning/phases/14-crm-attio-style-fix/14-RESEARCH.md
|
|
@.planning/phases/14-crm-attio-style-fix/14-PATTERNS.md
|
|
@.planning/phases/14-crm-attio-style-fix/14-UI-SPEC.md
|
|
</context>
|
|
|
|
<interfaces>
|
|
<!-- No new interfaces created; these are isolated bug fixes against existing types. -->
|
|
|
|
From src/lib/lead-validators.ts (existing, unchanged — CreateLeadInput already exported):
|
|
```typescript
|
|
export const LEAD_STAGES = ["contacted", "qualified", "proposal_sent", "negotiating", "won", "lost"] as const;
|
|
|
|
export const createLeadSchema = z.object({
|
|
name: z.string().min(1),
|
|
email: z.string().email().nullable().optional(),
|
|
phone: z.string().nullable().optional(),
|
|
company: z.string().nullable().optional(),
|
|
status: z.enum(LEAD_STAGES).optional(),
|
|
next_action: z.string().nullable().optional(),
|
|
// ... etc
|
|
});
|
|
|
|
export type CreateLeadInput = z.infer<typeof createLeadSchema>;
|
|
export type UpdateLeadInput = z.infer<typeof updateLeadSchema>;
|
|
```
|
|
|
|
From src/components/admin/dashboard/FollowUpWidget.tsx (current, full 47 lines — to be translated):
|
|
```tsx
|
|
import Link from "next/link";
|
|
import { getLeadsNeedingFollowUp } from "@/lib/lead-service";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export async function FollowUpWidget() {
|
|
const leads = await getLeadsNeedingFollowUp(7);
|
|
const count = leads.length;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Require Follow-up</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{count > 0 ? (
|
|
<>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
{count} lead{count !== 1 ? "s" : ""} to contact
|
|
</p>
|
|
<div className="space-y-2">
|
|
{leads.slice(0, 5).map((lead) => (
|
|
<div key={lead.id} className="flex items-center justify-between">
|
|
<span className="text-sm font-medium">{lead.name}</span>
|
|
<Button size="sm" variant="outline" asChild>
|
|
<Link href={`/admin/leads/${lead.id}`}>Contact</Link>
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{count > 5 && (
|
|
<Link href="/admin/leads" className="text-sm text-blue-600 hover:underline mt-2 block">
|
|
View All ({count})
|
|
</Link>
|
|
)}
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-gray-500">All leads are up to date!</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
From src/components/admin/leads/LeadForm.tsx (current, lines 1-40 and 202-220 — relevant excerpts):
|
|
```tsx
|
|
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { createLeadSchema } from "@/lib/lead-validators";
|
|
import type { z } from "zod";
|
|
import type { Lead } from "@/db/schema";
|
|
// ... other imports
|
|
|
|
type CreateLeadInput = z.infer<typeof createLeadSchema>; // <-- local redeclaration, line 36, to be removed
|
|
|
|
export function CreateLeadModal() {
|
|
const form = useForm<any>({ // <-- line ~42, to be fixed
|
|
resolver: zodResolver(createLeadSchema),
|
|
defaultValues: { name: "", email: "", phone: "", company: "", status: "contacted", next_action: "" },
|
|
});
|
|
// ...
|
|
}
|
|
|
|
// ... EditLeadModal starts around line 202
|
|
export function EditLeadModal({ lead, open, onOpenChange }: { lead: Lead; open: boolean; onOpenChange: (open: boolean) => void }) {
|
|
const form = useForm<any>({ // <-- to be fixed
|
|
resolver: zodResolver(createLeadSchema),
|
|
defaultValues: {
|
|
name: lead.name,
|
|
email: lead.email ?? "",
|
|
phone: lead.phone ?? "",
|
|
company: lead.company ?? "",
|
|
status: lead.status as any, // <-- line 212, to be narrowed
|
|
next_action: lead.next_action ?? "",
|
|
},
|
|
});
|
|
// ...
|
|
}
|
|
```
|
|
|
|
From src/components/admin/leads/SendQuoteModal.tsx (current, lines 31-75 and 120-135 — relevant excerpts):
|
|
```tsx
|
|
const assignQuoteSchema = z.object({
|
|
tab: z.enum(["existing", "new"]),
|
|
quote_id: z.string().optional(),
|
|
generate_new: z.boolean().optional(), // <-- dead field, line ~31
|
|
});
|
|
|
|
// ...
|
|
|
|
const form = useForm<AssignQuoteInput>({
|
|
resolver: zodResolver(assignQuoteSchema),
|
|
defaultValues: { tab: "existing", quote_id: "", generate_new: false }, // <-- line ~43
|
|
});
|
|
|
|
// ...
|
|
|
|
async function onSubmit(data: AssignQuoteInput) {
|
|
if (data.tab === "new") {
|
|
window.location.href = `/admin/quotes/new?lead_id=${lead.id}`; // <-- dead branch, lines 51-54, UNREACHABLE
|
|
return;
|
|
}
|
|
|
|
setError(null);
|
|
startTransition(async () => {
|
|
try {
|
|
const result = await assignQuoteToLead({ lead_id: lead.id, quote_id: data.quote_id! });
|
|
if (!result.success) {
|
|
setError(result.error ?? "Errore");
|
|
return;
|
|
}
|
|
onOpenChange(false);
|
|
router.refresh();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "Errore");
|
|
}
|
|
});
|
|
}
|
|
|
|
// ... around line 126-133, the "new" tab's button:
|
|
{tab === "new" && (
|
|
<Button type="button" onClick={() => { window.location.href = `/admin/quotes/new?lead_id=${lead.id}`; }}>
|
|
Crea Nuovo Preventivo
|
|
</Button>
|
|
)}
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Translate FollowUpWidget.tsx to Italian (CRM-10)</name>
|
|
<files>src/components/admin/dashboard/FollowUpWidget.tsx</files>
|
|
<read_first>
|
|
- src/components/admin/dashboard/FollowUpWidget.tsx (current, full 47-line file)
|
|
- .planning/phases/14-crm-attio-style-fix/14-UI-SPEC.md (Design Decision 5 — FollowUpWidget plural handling)
|
|
- .planning/phases/14-crm-attio-style-fix/14-RESEARCH.md (Pitfall 3 — FollowUpWidget i18n scope, confirms lead-service.ts has no English strings, fix is widget-only)
|
|
</read_first>
|
|
<action>
|
|
Translate all 6 hardcoded English strings in `FollowUpWidget.tsx` to Italian, per UI-SPEC.md Decision 5 (Italian "lead" is invariant — same word for singular/plural, so no conditional pluralization needed):
|
|
|
|
1. `"Require Follow-up"` (CardTitle) → `"Richiedi Follow-up"`
|
|
2. `{count} lead{count !== 1 ? "s" : ""} to contact` → `{count} lead da contattare` (remove the `{count !== 1 ? "s" : ""}` ternary entirely — "lead" never takes an "s" in Italian)
|
|
3. `"Contact"` (Button label) → `"Contatta"`
|
|
4. `View All ({count})` → `Vedi Tutto ({count})`
|
|
5. `"All leads are up to date!"` (empty state) → `"Tutti i lead sono aggiornati!"`
|
|
|
|
Apply these as direct string replacements — do not change any logic, imports, component structure, or the `getLeadsNeedingFollowUp(7)` call. Only the JSX text content changes.
|
|
|
|
The full corrected file:
|
|
|
|
```tsx
|
|
import Link from "next/link";
|
|
import { getLeadsNeedingFollowUp } from "@/lib/lead-service";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export async function FollowUpWidget() {
|
|
const leads = await getLeadsNeedingFollowUp(7);
|
|
const count = leads.length;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Richiedi Follow-up</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{count > 0 ? (
|
|
<>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
{count} lead da contattare
|
|
</p>
|
|
<div className="space-y-2">
|
|
{leads.slice(0, 5).map((lead) => (
|
|
<div key={lead.id} className="flex items-center justify-between">
|
|
<span className="text-sm font-medium">{lead.name}</span>
|
|
<Button size="sm" variant="outline" asChild>
|
|
<Link href={`/admin/leads/${lead.id}`}>Contatta</Link>
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{count > 5 && (
|
|
<Link href="/admin/leads" className="text-sm text-blue-600 hover:underline mt-2 block">
|
|
Vedi Tutto ({count})
|
|
</Link>
|
|
)}
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-gray-500">Tutti i lead sono aggiornati!</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>grep -ic "Require Follow-up\|to contact\|\"Contact\"\|View All\|up to date" src/components/admin/dashboard/FollowUpWidget.tsx | grep -qx 0 && npx tsc --noEmit</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -c "Richiedi Follow-up" src/components/admin/dashboard/FollowUpWidget.tsx` returns 1
|
|
- `grep -c "lead da contattare" src/components/admin/dashboard/FollowUpWidget.tsx` returns 1
|
|
- `grep -c "Contatta" src/components/admin/dashboard/FollowUpWidget.tsx` returns 1
|
|
- `grep -c "Vedi Tutto" src/components/admin/dashboard/FollowUpWidget.tsx` returns 1
|
|
- `grep -c "Tutti i lead sono aggiornati" src/components/admin/dashboard/FollowUpWidget.tsx` returns 1
|
|
- `grep -ic "Require Follow-up\|to contact\|\"Contact\"\|View All\|up to date" src/components/admin/dashboard/FollowUpWidget.tsx` returns 0 (no English strings remain)
|
|
- `grep -c 'count !== 1 ? "s" : ""' src/components/admin/dashboard/FollowUpWidget.tsx` returns 0 (pluralization ternary removed)
|
|
- `npx tsc --noEmit` exits 0
|
|
- `npx eslint src/components/admin/dashboard/FollowUpWidget.tsx` exits 0
|
|
</acceptance_criteria>
|
|
<done>FollowUpWidget.tsx contains only Italian text: "Richiedi Follow-up" heading, "{count} lead da contattare" body (no pluralization), "Contatta" button, "Vedi Tutto ({count})" link, "Tutti i lead sono aggiornati!" empty state. No English strings remain. Type-checks and lints cleanly.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Type LeadForm.tsx's useForm with CreateLeadInput (CRM-11)</name>
|
|
<files>src/components/admin/leads/LeadForm.tsx</files>
|
|
<read_first>
|
|
- src/components/admin/leads/LeadForm.tsx (current, full 366-line file — both CreateLeadModal lines 38-201 and EditLeadModal lines 202-365)
|
|
- src/lib/lead-validators.ts (full file — confirm CreateLeadInput export, createLeadSchema shape, LEAD_STAGES)
|
|
- .planning/phases/14-crm-attio-style-fix/14-RESEARCH.md (relevant Pattern for CRM-11 fix)
|
|
</read_first>
|
|
<behavior>
|
|
Manual-trace behaviors (verified via `npx tsc --noEmit` — TypeScript itself is the test for this type-safety fix):
|
|
- `CreateLeadModal`'s `useForm<any>()` becomes `useForm<CreateLeadInput>()`, where `CreateLeadInput` is imported from `@/lib/lead-validators` (not locally redeclared)
|
|
- `EditLeadModal`'s `useForm<any>()` becomes `useForm<CreateLeadInput>()`
|
|
- The local `type CreateLeadInput = z.infer<typeof createLeadSchema>;` declaration (current line 36) is REMOVED — the type now comes from the import
|
|
- `EditLeadModal`'s `defaultValues.status: lead.status as any` becomes `status: lead.status as CreateLeadInput["status"]` — the only remaining cast, narrowing `Lead.status` (a plain `string` column type from Drizzle's `$inferSelect`) to the Zod-enum-derived `CreateLeadInput["status"]` type (`"contacted" | "qualified" | "proposal_sent" | "negotiating" | "won" | "lost" | undefined`)
|
|
- All `form.register(...)`, `form.handleSubmit(...)`, `form.setValue(...)`, `form.watch(...)` calls (wherever they appear in both modals) continue to type-check against `CreateLeadInput` field names (`name`, `email`, `phone`, `company`, `status`, `next_action`, etc.) — if any currently-untyped `any`-cast field access breaks, the field name is wrong relative to `createLeadSchema` and must be corrected to match the schema (do not re-introduce `any` to suppress the error)
|
|
- If `z` import becomes unused after removing the local `type CreateLeadInput = z.infer<...>` line, remove the now-unused `import type { z } from "zod"` (or equivalent) — but only if `z` is not used elsewhere in the file (grep first)
|
|
</behavior>
|
|
<action>
|
|
1. Read the full `LeadForm.tsx` file to locate:
|
|
- The current local type declaration: `type CreateLeadInput = z.infer<typeof createLeadSchema>;`
|
|
- Both `useForm<any>({...})` call sites (one in `CreateLeadModal`, one in `EditLeadModal`)
|
|
- The `status: lead.status as any` cast in `EditLeadModal`'s `defaultValues`
|
|
- The current import block (to determine whether `z` and `createLeadSchema` are already imported, and from where)
|
|
|
|
2. Add `CreateLeadInput` to the import from `@/lib/lead-validators`:
|
|
```tsx
|
|
import { createLeadSchema, type CreateLeadInput } from "@/lib/lead-validators";
|
|
```
|
|
(adjust based on the exact current import statement — if `createLeadSchema` is already imported from `lead-validators`, just add `, type CreateLeadInput` to that same import line)
|
|
|
|
3. Remove the local redeclaration: delete the line `type CreateLeadInput = z.infer<typeof createLeadSchema>;` entirely.
|
|
|
|
4. If `import type { z } from "zod"` (or `import { z } from "zod"` used only for this) becomes unused after step 3, remove it. First grep the file for other `z.` usages — if any remain (e.g., inline schema validation elsewhere in the file), keep the import.
|
|
|
|
5. In `CreateLeadModal`, change:
|
|
```tsx
|
|
const form = useForm<any>({
|
|
```
|
|
to:
|
|
```tsx
|
|
const form = useForm<CreateLeadInput>({
|
|
```
|
|
|
|
6. In `EditLeadModal`, change:
|
|
```tsx
|
|
const form = useForm<any>({
|
|
```
|
|
to:
|
|
```tsx
|
|
const form = useForm<CreateLeadInput>({
|
|
```
|
|
|
|
7. In `EditLeadModal`'s `defaultValues`, change:
|
|
```tsx
|
|
status: lead.status as any,
|
|
```
|
|
to:
|
|
```tsx
|
|
status: lead.status as CreateLeadInput["status"],
|
|
```
|
|
|
|
8. Run `npx tsc --noEmit`. If new type errors surface inside either modal (e.g., a `form.watch("someField")` call referencing a field name not in `createLeadSchema`, or a `form.setValue(...)` with a mismatched type), fix the call site to match the actual `createLeadSchema` shape (from `lead-validators.ts`) — do NOT reintroduce `any` or `as any` to silence the error. The goal is genuine type safety, not error suppression.
|
|
</action>
|
|
<verify>
|
|
<automated>grep -c "useForm<any>\|as any" src/components/admin/leads/LeadForm.tsx | grep -qx 0 && npx tsc --noEmit && npx eslint src/components/admin/leads/LeadForm.tsx</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -c "useForm<any>" src/components/admin/leads/LeadForm.tsx` returns 0
|
|
- `grep -c "useForm<CreateLeadInput>" src/components/admin/leads/LeadForm.tsx` returns 2 (CreateLeadModal + EditLeadModal)
|
|
- `grep -c "type CreateLeadInput = z.infer" src/components/admin/leads/LeadForm.tsx` returns 0 (local redeclaration removed)
|
|
- `grep -c "CreateLeadInput" src/components/admin/leads/LeadForm.tsx | grep -v '^0'` returns non-empty (import line + 2 useForm + 1 cast = at least 4 occurrences)
|
|
- `grep -c "status: lead.status as any" src/components/admin/leads/LeadForm.tsx` returns 0
|
|
- `grep -c 'status: lead.status as CreateLeadInput\["status"\]' src/components/admin/leads/LeadForm.tsx` returns 1
|
|
- `grep -c "as any" src/components/admin/leads/LeadForm.tsx` returns 0 (no remaining `any` casts in the file)
|
|
- `npx tsc --noEmit` exits 0
|
|
- `npx eslint src/components/admin/leads/LeadForm.tsx` exits 0
|
|
</acceptance_criteria>
|
|
<done>Both CreateLeadModal and EditLeadModal use useForm<CreateLeadInput>() with CreateLeadInput imported from lead-validators.ts (no local redeclaration). The only remaining type assertion is the narrowing cast `lead.status as CreateLeadInput["status"]` in EditLeadModal's defaultValues. No `as any` remains anywhere in the file. Type-checks and lints cleanly.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 3: Remove dead onSubmit branch in SendQuoteModal.tsx (CRM-12)</name>
|
|
<files>src/components/admin/leads/SendQuoteModal.tsx</files>
|
|
<read_first>
|
|
- src/components/admin/leads/SendQuoteModal.tsx (current, full 140-line file)
|
|
- .planning/phases/14-crm-attio-style-fix/14-RESEARCH.md (Pitfall 4 — SendQuoteModal dead branch analysis)
|
|
- .planning/phases/14-crm-attio-style-fix/14-PATTERNS.md (SendQuoteModal.tsx fixed pattern)
|
|
</read_first>
|
|
<behavior>
|
|
Manual-trace behaviors (verified via `npx tsc --noEmit` + `npx eslint` — no existing test file for this component; the fix is a dead-code removal, behavior for the "existing" tab path is unchanged):
|
|
- `onSubmit`'s body NO LONGER contains an `if (data.tab === "new") { window.location.href = ...; return; }` branch — this branch was unreachable because: (a) the "new" tab's submit button has its own `onClick={() => window.location.href = ...}` handler that navigates directly without calling `form.handleSubmit`, and (b) when `tab === "new"`, the only rendered button is that direct-navigation button — no `type="submit"` button exists for the "new" tab, so `onSubmit` (wired to `form.handleSubmit(onSubmit)`) can never fire with `data.tab === "new"`
|
|
- `onSubmit` now begins directly with the "existing" tab logic: `setError(null); startTransition(async () => { ... assignQuoteToLead({ lead_id: lead.id, quote_id: data.quote_id! }) ... })`
|
|
- The "new" tab's button (with its own `onClick` navigation) is UNCHANGED — it continues to work exactly as before
|
|
- `assignQuoteToLead({ lead_id, quote_id })` call signature is unchanged — only the dead branch above it is removed
|
|
</behavior>
|
|
<action>
|
|
1. Read the full `SendQuoteModal.tsx` file.
|
|
|
|
2. Locate the `onSubmit` function. It currently begins with:
|
|
```tsx
|
|
async function onSubmit(data: AssignQuoteInput) {
|
|
if (data.tab === "new") {
|
|
window.location.href = `/admin/quotes/new?lead_id=${lead.id}`;
|
|
return;
|
|
}
|
|
|
|
setError(null);
|
|
startTransition(async () => {
|
|
// ... existing-tab logic
|
|
});
|
|
}
|
|
```
|
|
|
|
3. Remove the dead `if (data.tab === "new") { ... return; }` block entirely, so `onSubmit` begins directly with `setError(null);`:
|
|
```tsx
|
|
async function onSubmit(data: AssignQuoteInput) {
|
|
setError(null);
|
|
startTransition(async () => {
|
|
// ... existing-tab logic (unchanged)
|
|
});
|
|
}
|
|
```
|
|
|
|
4. Confirm the "new" tab's button (its own `onClick={() => { window.location.href = ... }}`, separate from form submission) is untouched — it remains the sole navigation path for the "new" tab.
|
|
|
|
5. Leave the `generate_new: z.boolean().optional()` field in `assignQuoteSchema` and its `defaultValues: { ..., generate_new: false }` entry AS-IS — RESEARCH.md flags this as optional cleanup, not required for CRM-12. Removing it would require also checking `AssignQuoteInput` usages elsewhere (out of scope for this isolated fix). Do not touch it.
|
|
|
|
6. After the edit, grep the file for `data.tab` to confirm no other dead `tab === "new"` checks remain inside `onSubmit`. If the `tab` field is still referenced elsewhere (e.g., to conditionally render the "existing" vs "new" tab UI in JSX), that is correct and expected — only the `onSubmit` dead branch is removed.
|
|
</action>
|
|
<verify>
|
|
<automated>grep -A2 "async function onSubmit" src/components/admin/leads/SendQuoteModal.tsx | grep -c 'data.tab === "new"' | grep -qx 0 && npx tsc --noEmit && npx eslint src/components/admin/leads/SendQuoteModal.tsx</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -A2 "async function onSubmit" src/components/admin/leads/SendQuoteModal.tsx | grep -c 'data.tab === "new"'` returns 0
|
|
- `grep -c "window.location.href" src/components/admin/leads/SendQuoteModal.tsx` returns 1 (only the "new" tab button's onClick remains, the onSubmit copy is removed)
|
|
- `grep -c "assignQuoteToLead" src/components/admin/leads/SendQuoteModal.tsx` returns at least 1 (existing-tab submit logic preserved)
|
|
- `grep -c "generate_new" src/components/admin/leads/SendQuoteModal.tsx` returns at least 1 (left untouched per action step 5)
|
|
- `npx tsc --noEmit` exits 0
|
|
- `npx eslint src/components/admin/leads/SendQuoteModal.tsx` exits 0
|
|
</acceptance_criteria>
|
|
<done>SendQuoteModal.tsx's onSubmit no longer contains the unreachable `if (data.tab === "new") {...}` branch — onSubmit now starts directly with `setError(null)` and the existing-tab assignQuoteToLead logic. The "new" tab's own onClick-based navigation button is unchanged. generate_new field left as-is (optional cleanup, out of scope). Type-checks and lints cleanly.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|--------------|
|
|
| Admin browser -> FollowUpWidget/LeadForm/SendQuoteModal | Server-rendered (FollowUpWidget) and client components (LeadForm, SendQuoteModal); no new data flows introduced |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-14-11 | Tampering | LeadForm.tsx — `useForm<any>()` to `useForm<CreateLeadInput>()` | mitigate | Stronger compile-time typing reduces risk of malformed form payloads reaching `createLead`/`updateLead` server actions; Zod validation at the action boundary (pre-existing, unchanged) remains the actual runtime enforcement — this fix is a defense-in-depth/DX improvement, not a new security control |
|
|
| T-14-12 | Tampering | SendQuoteModal.tsx — dead `onSubmit` branch removal | accept | Removing unreachable code has no security effect; `assignQuoteToLead` (existing-tab path) and its `requireAdmin`-equivalent guards (if any, pre-existing) are unchanged |
|
|
| T-14-13 | Information Disclosure | FollowUpWidget.tsx — string translation only | accept | No data exposure change; only display copy is translated. `getLeadsNeedingFollowUp(7)` query and its session-protected `/admin` route context are unchanged |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
1. `npx tsc --noEmit` — exits 0 across all three modified files
|
|
2. `npx eslint src/components/admin/dashboard/FollowUpWidget.tsx src/components/admin/leads/LeadForm.tsx src/components/admin/leads/SendQuoteModal.tsx` — exits 0
|
|
3. `grep -ic "Require Follow-up\|to contact\|View All\|up to date" src/components/admin/dashboard/FollowUpWidget.tsx` returns 0 (no English strings)
|
|
4. `grep -c "useForm<any>\|as any" src/components/admin/leads/LeadForm.tsx` returns 0
|
|
5. `grep -A2 "async function onSubmit" src/components/admin/leads/SendQuoteModal.tsx | grep -c 'data.tab === "new"'` returns 0
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- FollowUpWidget.tsx displays only Italian text ("Richiedi Follow-up", "{count} lead da contattare", "Contatta", "Vedi Tutto ({count})", "Tutti i lead sono aggiornati!") — CRM-10 satisfied
|
|
- LeadForm.tsx's CreateLeadModal and EditLeadModal both use `useForm<CreateLeadInput>()` with `CreateLeadInput` imported from `lead-validators.ts`; no `as any` remains — CRM-11 satisfied
|
|
- SendQuoteModal.tsx's `onSubmit` no longer contains the unreachable `tab === "new"` branch; existing-tab submission and new-tab button navigation both continue to work — CRM-12 satisfied
|
|
- `npx tsc --noEmit` and `npx eslint` both exit 0 across all three files
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/14-crm-attio-style-fix/14-03-SUMMARY.md`
|
|
</output>
|