Initial commit: thbd-portal, duplicated from elmi-portal
Rebranded for Thunderbird Portal (THBD): numbering prefix EL->TH, all "ELMI"/"ЭЛМИ" UI strings genericized, ELMI's real contract templates and company legal/bank details removed (not applicable to this company), fresh VAPID keypair and encryption key, fresh single-migration schema history. Same architecture as elmi-portal: Web Push notifications, unread-chat app badge, cookie-session auth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
commit
579528edf7
197 files changed
+25022
No files matched your search
@@ -0,0 +1,78 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getCurrentSession } from "@/lib/auth/session";
|
||||
import { listTemplates } from "@/lib/documents/templates";
|
||||
import { UploadTemplateForm } from "./upload-form";
|
||||
|
||||
const KIND_LABEL: Record<string, string> = {
|
||||
power_of_attorney: "Доверенность",
|
||||
forwarder_receipt: "Экспедиторская расписка",
|
||||
contract_teo_forwarder: "Договор ТЭУ (наша компания — Экспедитор)",
|
||||
contract_teo_customer: "Договор ТЭО (наша компания — Заказчик)",
|
||||
appendix_order_forwarder: "Приложение — Поручение + Расписка (наша компания — Экспедитор)",
|
||||
appendix_order_customer: "Приложение — Поручение + Расписка (наша компания — Заказчик)",
|
||||
order_agreement_road_forwarder: "Договор-заявка на автоперевозку (наша компания — Экспедитор)",
|
||||
order_agreement_teo_customer: "Договор-заявка на ТЭО (наша компания — Заказчик)",
|
||||
other: "Другое",
|
||||
};
|
||||
|
||||
export default async function TemplatesPage() {
|
||||
const session = await getCurrentSession();
|
||||
if (!session) redirect("/orders");
|
||||
|
||||
const templates = await listTemplates();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h1 className="font-display text-xl font-bold tracking-tight">Шаблоны документов</h1>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-sm text-text-muted">
|
||||
Стартовые шаблоны загружаются в Настройки → Шаблоны. Загружая свой .docx взамен, используйте теги: заявка —{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{order_number}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{order_date}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{contract_date}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{cargo_description}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{route_from}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{route_to}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{flight_number}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{manager_name}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{load_date}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{unload_plan_date}"}</code>; доверенность —{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{poa_number}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{poa_date}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{poa_valid_until}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{poa_driver_name}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{poa_driver_passport}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{poa_vehicle_model}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{poa_vehicle_plate}"}</code>; реквизиты сторон — с
|
||||
префиксом <code className="rounded bg-surface-hover px-1">{"company_*"}</code> (наша компания, из Настройки → Компания)
|
||||
и <code className="rounded bg-surface-hover px-1">{"customer_*"}</code> (контрагент заявки), например{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{company_name}"}</code>,{" "}
|
||||
<code className="rounded bg-surface-hover px-1">{"{customer_signatory_short}"}</code>. Полный список — в{" "}
|
||||
<code className="rounded bg-surface-hover px-1">buildTemplateData</code>.
|
||||
</p>
|
||||
|
||||
<div className="card mb-5 overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left text-text-muted">
|
||||
<th className="px-4 py-3 font-medium">Название</th>
|
||||
<th className="px-4 py-3 font-medium">Тип</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{templates.map((t) => (
|
||||
<tr key={t.id} className="border-b border-border last:border-0">
|
||||
<td className="px-4 py-3">{t.name}</td>
|
||||
<td className="px-4 py-3 text-text-muted">{KIND_LABEL[t.kind]}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<UploadTemplateForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const KIND_OPTIONS = [
|
||||
{ value: "power_of_attorney", label: "Доверенность" },
|
||||
{ value: "forwarder_receipt", label: "Экспедиторская расписка" },
|
||||
{ value: "contract_teo_forwarder", label: "Договор ТЭУ (наша компания — Экспедитор)" },
|
||||
{ value: "contract_teo_customer", label: "Договор ТЭО (наша компания — Заказчик)" },
|
||||
{ value: "appendix_order_forwarder", label: "Приложение — Поручение + Расписка (наша компания — Экспедитор)" },
|
||||
{ value: "appendix_order_customer", label: "Приложение — Поручение + Расписка (наша компания — Заказчик)" },
|
||||
{ value: "order_agreement_road_forwarder", label: "Договор-заявка на автоперевозку (наша компания — Экспедитор)" },
|
||||
{ value: "order_agreement_teo_customer", label: "Договор-заявка на ТЭО (наша компания — Заказчик)" },
|
||||
{ value: "other", label: "Другое" },
|
||||
];
|
||||
|
||||
export function UploadTemplateForm() {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
const [kind, setKind] = useState("power_of_attorney");
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!file) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const form = new FormData();
|
||||
form.set("name", name);
|
||||
form.set("kind", kind);
|
||||
form.set("file", file);
|
||||
|
||||
const res = await fetch("/api/templates", { method: "POST", body: form });
|
||||
setLoading(false);
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
setError(data?.error ?? "Не удалось загрузить шаблон");
|
||||
return;
|
||||
}
|
||||
setName("");
|
||||
setFile(null);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="card flex flex-wrap items-end gap-2 p-4">
|
||||
<label className="text-sm">
|
||||
<span className="mb-1 block font-medium text-text-muted">Название</span>
|
||||
<input
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="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>
|
||||
<select
|
||||
value={kind}
|
||||
onChange={(e) => setKind(e.target.value)}
|
||||
className="rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
|
||||
>
|
||||
{KIND_OPTIONS.map((k) => (
|
||||
<option key={k.value} value={k.value}>
|
||||
{k.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-sm">
|
||||
<span className="mb-1 block font-medium text-text-muted">Файл .docx</span>
|
||||
<input
|
||||
type="file"
|
||||
required
|
||||
accept=".docx"
|
||||
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
|
||||
className="rounded-md border border-border bg-surface px-3 py-2 outline-none focus:border-accent"
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={loading} className="btn btn-primary">
|
||||
{loading ? "Загружаем…" : "Загрузить"}
|
||||
</button>
|
||||
{error && <p className="w-full text-sm text-danger-soft-text">{error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user