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:
1 parent
e9e05d859d
commit
6cce8f890f
49 files changed
+5931
-137
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user