/* documents.jsx — Документы: паспорт/права/СНИЛС и др., свои и чужие, с авто-распознаванием по фото. */ const DOC_TYPES = [ { v: "passport", label: "Паспорт" }, { v: "license", label: "Права" }, { v: "snils", label: "СНИЛС" }, { v: "inn", label: "ИНН" }, { v: "other", label: "Другое" }, ]; const docTypeLabel = (t) => (DOC_TYPES.find((x) => x.v === t) || { label: "Документ" }).label; function DocumentsScreen() { const [items, setItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const [q, setQ] = React.useState(""); const [openId, setOpenId] = React.useState(null); const [scanning, setScanning] = React.useState(false); const [err, setErr] = React.useState(""); const fileRef = React.useRef(null); const load = React.useCallback(async () => { setLoading(true); setItems(await api.getDocuments({ q: q || undefined })); setLoading(false); }, [q]); React.useEffect(() => { const t = setTimeout(load, 200); return () => clearTimeout(t); }, [q]); const onFile = async (e) => { const f = (e.target.files || [])[0]; e.target.value = ""; if (!f) return; setErr(""); setScanning(true); try { const doc = await api.scanDocument(f); await load(); setOpenId(doc.id); // открываем для проверки распознанного } catch (er) { setErr("Не удалось распознать. Попробуй фото чётче или добавь вручную."); } finally { setScanning(false); } }; const addBlank = async () => { const doc = await api.createDocument({ doc_type: "passport", title: "Паспорт" }); await load(); setOpenId(doc.id); }; return (
Загрузи фото документа — данные распознаются автоматически. Хранилище личное; держи доступ к аккаунту в секрете.
{err &&

{err}

}
setQ(e.target.value)} placeholder="Поиск по владельцу, типу, заметке…" className="w-full h-11 rounded-xl border border-[var(--border)] bg-[var(--surface)] pl-10 pr-3 text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition placeholder:text-[var(--muted)]" />
{loading ? (
{[0, 1].map((i) =>
)}
) : items.length === 0 ? ( ) : (
{items.map((d, i) => setOpenId(d.id)} />)}
)} setOpenId(null)} onChanged={load} />
); } function DocumentRow({ d, index = 0, onOpen }) { const photo = (d.photos || [])[0]; return ( ); } function DocumentModal({ docId, open, onClose, onChanged }) { const [data, setData] = React.useState(null); const [form, setForm] = React.useState({}); const [rows, setRows] = React.useState([]); // [{k,v}] const [busy, setBusy] = React.useState(false); const [confirmDel, setConfirmDel] = React.useState(false); const photoRef = React.useRef(null); const fill = (d) => { setData(d); setForm({ owner_name: d.owner_name || "", doc_type: d.doc_type || "other", title: d.title || "", note: d.note || "" }); setRows(Object.entries(d.fields || {}).map(([k, v]) => ({ k, v }))); }; React.useEffect(() => { if (open && docId) { setData(null); setConfirmDel(false); api.getDocument(docId).then(fill); } }, [open, docId]); const set = (k, v) => setForm((f) => ({ ...f, [k]: v })); const setRow = (i, key, val) => setRows((r) => r.map((x, j) => (j === i ? { ...x, [key]: val } : x))); const addRow = () => setRows((r) => [...r, { k: "", v: "" }]); const delRow = (i) => setRows((r) => r.filter((_, j) => j !== i)); const save = async () => { setBusy(true); const fields = {}; rows.forEach((x) => { if (x.k.trim()) fields[x.k.trim()] = x.v; }); await api.updateDocument(docId, { ...form, fields }); setBusy(false); onChanged && onChanged(); onClose(); }; const del = async () => { setBusy(true); await api.deleteDocument(docId); setBusy(false); onChanged && onChanged(); onClose(); }; const onPhoto = async (e) => { const f = (e.target.files || [])[0]; e.target.value = ""; if (!f) return; setBusy(true); try { const d = await api.addDocumentPhoto(docId, f); fill(d); } catch (er) {} finally { setBusy(false); } }; return (

{form.title || "Документ"}

{!data ? (
) : ( <>
{/* фото */}
{(data.photos || []).map((p, i) => ( ))}
set("owner_name", e.target.value)} placeholder="Мой / ФИО" className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-[42px] text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
set("title", e.target.value)} className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-[42px] text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" /> {/* поля документа */}
Данные
{rows.map((r, i) => (
setRow(i, "k", e.target.value)} placeholder="поле" className="w-2/5 rounded-lg border border-[var(--border)] bg-[var(--surface-2)] px-3 h-10 text-[13px] text-[var(--muted)] outline-none focus:border-[var(--accent)] transition" /> setRow(i, "v", e.target.value)} placeholder="значение" className="flex-1 rounded-lg border border-[var(--border)] bg-[var(--surface-2)] px-3 h-10 text-[13px] text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
))}
set("note", e.target.value)} className="w-full rounded-xl border border-[var(--border)] bg-[var(--surface-2)] px-3.5 h-[42px] text-sm text-[var(--text)] outline-none focus:border-[var(--accent)] transition" />
{!confirmDel ? ( ) : (
)}
)}
); } Object.assign(window, { DocumentsScreen });