Add Lead -> Request -> Order funnel redesign (Phase 2)

New leads entity (own numbering VTH{yy}-{seq}, 2-stage funnel, converts
into inquiries) sitting ahead of the existing Request/Order pipeline.
Request funnel expanded from 4 to 6 statuses (kp_preparation -> kp_response
-> negotiation -> signing -> converted/declined) and Order funnel from 4
to 11 (new -> ... -> completed/declined), matching the stage names/fields/
decline-reasons from the flowcharts. Decline now always requires a reason
(+ free-text detail for "other"), reused across all three entities via one
DeclineWithReason component. Added shared deal-reference fields (Incoterms,
payment terms, carrier1-3, insurance company, customs broker) to both
inquiries and orders, carried over on conversion. Added a minimal outbound
SMTP capability (reusing the existing mailbox credentials) so order
status changes can best-effort email the client, matching the stages the
diagrams call out. Per-stage informational panels (requirements + transition
criteria) shown on each entity's detail page — structure only, no SLA-timer
automation yet (explicitly deferred, per this session's scoping).

Fixed a drizzle-kit migration-generation bug by hand: the generated
INSERT...SELECT for the inquiries table rebuild referenced brand-new
columns on the pre-migration source table, which don't exist there yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-08-31 18:24:06 +00:00
1 parent e9e05d859d
commit 6cce8f890f
49 files changed
+5931 -137

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
import Link from "next/link";
interface LeadItem {
id: string;
number: string;
status: "new" | "in_progress" | "converted" | "declined";
firstName: string | null;
lastName: string | null;
companyName: string | null;
cargoDescription: string | null;
createdAt: Date;
responsible: { name: string } | null;
}
interface Column {
status: LeadItem["status"];
label: string;
}
function toDateInputValue(date: Date): string {
const d = new Date(date);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
export function LeadsBoard({ leads, columns }: { leads: LeadItem[]; columns: Column[] }) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{columns.map((col) => {
const items = leads.filter((l) => l.status === col.status);
return (
<div key={col.status}>
<h2 className="mb-2 flex items-center gap-2 text-sm font-semibold text-text-muted">
{col.label}
<span className="rounded-full bg-surface-hover px-1.5 py-0.5 text-xs">{items.length}</span>
</h2>
<div className="flex flex-col gap-2">
{items.map((lead) => (
<Link key={lead.id} href={`/leads/${lead.id}`} className="card block p-3 hover:border-accent">
<div className="mb-1 flex items-center justify-between">
<span className="font-mono text-xs text-text-faint">{lead.number}</span>
</div>
<div className="mb-1 truncate text-sm font-medium">
{[lead.lastName, lead.firstName].filter(Boolean).join(" ") || lead.companyName || "Без имени"}
</div>
{lead.cargoDescription && <div className="truncate text-xs text-text-muted">{lead.cargoDescription}</div>}
<div className="mt-1 flex items-center justify-between text-xs text-text-faint">
<span>{lead.responsible?.name ?? "—"}</span>
<span>{toDateInputValue(lead.createdAt)}</span>
</div>
</Link>
))}
{items.length === 0 && <p className="text-xs text-text-faint">Пусто</p>}
</div>
</div>
);
})}
</div>
);
}