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
60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
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>
|
|
);
|
|
}
|