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

@@ -9,6 +9,8 @@ interface Status {
user: string | null;
imapHost: string | null;
verifiedAt: number | null;
smtpHost: string | null;
smtpPort: number | null;
}
export function MailboxForm({ status }: { status: Status }) {
@@ -18,6 +20,8 @@ export function MailboxForm({ status }: { status: Status }) {
const [user, setUser] = useState(status.user ?? "");
const [password, setPassword] = useState("");
const [allowInsecureTls, setAllowInsecureTls] = useState(false);
const [smtpHost, setSmtpHost] = useState(status.smtpHost ?? "");
const [smtpPort, setSmtpPort] = useState(status.smtpPort?.toString() ?? "465");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -29,7 +33,15 @@ export function MailboxForm({ status }: { status: Status }) {
const res = await fetch("/api/settings/mailbox", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ imapHost, imapPort: Number(imapPort), user, password, allowInsecureTls }),
body: JSON.stringify({
imapHost,
imapPort: Number(imapPort),
user,
password,
allowInsecureTls,
smtpHost: smtpHost || undefined,
smtpPort: smtpPort ? Number(smtpPort) : undefined,
}),
});
setLoading(false);
@@ -109,6 +121,30 @@ export function MailboxForm({ status }: { status: Status }) {
<span className="text-text-muted">Разрешить недоверенный TLS-сертификат</span>
</label>
<div className="sm:col-span-2 border-t border-border pt-3">
<p className="mb-2 text-sm font-medium text-text-muted">
Исходящая почта (SMTP) — для уведомлений клиенту о статусе заявки. Тот же логин/пароль, что выше. Необязательно.
</p>
</div>
<label className="text-sm">
<span className="mb-1 block font-medium text-text-muted">SMTP-сервер</span>
<input
value={smtpHost}
onChange={(e) => setSmtpHost(e.target.value)}
placeholder="smtp.yandex.ru"
className="w-full rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
/>
</label>
<label className="text-sm">
<span className="mb-1 block font-medium text-text-muted">Порт</span>
<input
type="number"
value={smtpPort}
onChange={(e) => setSmtpPort(e.target.value)}
className="w-full rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
/>
</label>
{error && <p className="sm:col-span-2 rounded-md bg-danger-soft px-3 py-2 text-sm text-danger-soft-text">{error}</p>}
<div className="flex gap-2 sm:col-span-2">