Files
thbd-portal/src/app/(admin)/settings/email/mailbox-form.tsx
T
ogrechkoandClaude Sonnet 5 6cce8f890f 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
2026-08-31 18:24:06 +00:00

164 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}