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
164 lines
6.3 KiB
TypeScript
164 lines
6.3 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
|
||
interface Status {
|
||
configured: boolean;
|
||
enabled: boolean;
|
||
user: string | null;
|
||
imapHost: string | null;
|
||
verifiedAt: number | null;
|
||
smtpHost: string | null;
|
||
smtpPort: number | null;
|
||
}
|
||
|
||
export function MailboxForm({ status }: { status: Status }) {
|
||
const router = useRouter();
|
||
const [imapHost, setImapHost] = useState(status.imapHost ?? "");
|
||
const [imapPort, setImapPort] = useState("993");
|
||
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);
|
||
|
||
async function handleSubmit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
const res = await fetch("/api/settings/mailbox", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
imapHost,
|
||
imapPort: Number(imapPort),
|
||
user,
|
||
password,
|
||
allowInsecureTls,
|
||
smtpHost: smtpHost || undefined,
|
||
smtpPort: smtpPort ? Number(smtpPort) : undefined,
|
||
}),
|
||
});
|
||
|
||
setLoading(false);
|
||
if (!res.ok) {
|
||
const data = await res.json().catch(() => null);
|
||
setError(data?.error ?? "Не удалось подключиться к почтовому ящику");
|
||
return;
|
||
}
|
||
setPassword("");
|
||
router.refresh();
|
||
}
|
||
|
||
async function handleDisable() {
|
||
setLoading(true);
|
||
await fetch("/api/settings/mailbox", { method: "DELETE" });
|
||
setLoading(false);
|
||
router.refresh();
|
||
}
|
||
|
||
return (
|
||
<div className="card p-4">
|
||
<h2 className="mb-3 text-sm font-semibold text-text-muted">Почтовый ящик для запросов (IMAP)</h2>
|
||
|
||
<div className="mb-4 text-sm">
|
||
{status.configured ? (
|
||
<p className="text-text-muted">
|
||
{status.enabled ? "Активен" : "Отключен"}: {status.user} @ {status.imapHost}
|
||
{status.verifiedAt && ` — проверен ${new Date(status.verifiedAt).toLocaleString("ru-RU")}`}
|
||
</p>
|
||
) : (
|
||
<p className="text-text-faint">Ещё не настроен.</p>
|
||
)}
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||
<label className="text-sm">
|
||
<span className="mb-1 block font-medium text-text-muted">IMAP-сервер</span>
|
||
<input
|
||
required
|
||
value={imapHost}
|
||
onChange={(e) => setImapHost(e.target.value)}
|
||
placeholder="imap.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"
|
||
required
|
||
value={imapPort}
|
||
onChange={(e) => setImapPort(e.target.value)}
|
||
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
|
||
required
|
||
value={user}
|
||
onChange={(e) => setUser(e.target.value)}
|
||
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="password"
|
||
required
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
className="w-full rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
|
||
/>
|
||
</label>
|
||
<label className="flex items-center gap-2 text-sm sm:col-span-2">
|
||
<input type="checkbox" checked={allowInsecureTls} onChange={(e) => setAllowInsecureTls(e.target.checked)} />
|
||
<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">
|
||
<button type="submit" disabled={loading} className="btn btn-primary">
|
||
{loading ? "Проверяем…" : "Проверить и сохранить"}
|
||
</button>
|
||
{status.configured && status.enabled && (
|
||
<button type="button" onClick={handleDisable} disabled={loading} className="btn btn-ghost">
|
||
Отключить
|
||
</button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|