primarydraft / app /frontend /src /components /CinIntake.tsx
Bot-Derpy's picture
PrimaryDraft: SEBI TechSprint prototype (27 deterministic checks, 40 real BSE SME issuers)
baf8a7b verified
Raw History Blame Contribute Delete
6.85 kB
import { useEffect, useState } from "react";
import { IdentificationCard, ArrowRight, WarningCircle } from "@phosphor-icons/react";
import { api, type RunSummary, ApiError } from "../lib/api";
import { ErrorNote, Note, Spinner } from "./common";
interface Parsed {
raw: string; valid: boolean; error: string;
is_listed: boolean | null; industry_code: string; nic_division_name: string;
state: string; state_code: string; year: number | null; age_years: number | null;
ownership_code: string; ownership: string; is_public_company: boolean | null;
registration_number: string; listing: string;
}
/** Intake for a pre-IPO issuer: no exchange history, so the CIN is the only
* record that exists until documents are supplied. */
export function CinIntake({ onStarted }: { onStarted: (run: RunSummary) => void }) {
const [cin, setCin] = useState("");
const [name, setName] = useState("");
const [parsed, setParsed] = useState<Parsed | null>(null);
const [error, setError] = useState<string | null>(null);
const [starting, setStarting] = useState(false);
useEffect(() => {
const v = cin.trim().toUpperCase();
if (v.length < 21) { setParsed(null); return; }
const t = setTimeout(() => {
api.inspectCin(v).then(setParsed).catch(() => setParsed(null));
}, 150);
return () => clearTimeout(t);
}, [cin]);
const start = async () => {
setStarting(true); setError(null);
try {
onStarted(await api.createRunFromCin(cin.trim().toUpperCase(), name));
} catch (e) {
setError(e instanceof ApiError ? e.message : String(e));
setStarting(false);
}
};
const seg = (label: string, value: string, chars: string) => (
<div style={{ display: "grid", gridTemplateColumns: "116px 1fr", gap: 10, fontSize: 12.5 }}>
<span style={{ color: "var(--text-2)" }}>{label}</span>
<span>
<span className="mono">{value}</span>
<span className="f-id" style={{ display: "block", marginTop: 1 }}>{chars}</span>
</span>
</div>
);
return (
<div className="grid cols-sidebar">
<section className="panel">
<div className="panel-h">
<IdentificationCard size={16} />
<h2>Pre-IPO issuer</h2>
</div>
<div className="panel-b">
<Note tone="quiet">
An unlisted company has no exchange filing history, so it cannot be found by
search. Its Corporate Identity Number is a real registry identifier and settles
several eligibility questions on its own. Financial disclosures stay absent until
documents are supplied, and the draft will say so rather than fill them in.
</Note>
<div className="field" style={{ marginTop: 14 }}>
<label htmlFor="cin">Corporate Identity Number</label>
<input id="cin" className="ctl" placeholder="U62090KA2025PTC207753"
maxLength={21} value={cin}
onChange={(e) => setCin(e.target.value.toUpperCase())} />
<div className="help">21 characters, as issued by the Ministry of Corporate Affairs.</div>
</div>
<div className="field">
<label htmlFor="coname">Registered name (optional)</label>
<input id="coname" className="ctl" style={{ fontFamily: "var(--sans)" }}
placeholder="As on the certificate of incorporation"
value={name} onChange={(e) => setName(e.target.value)} />
<div className="help">
Recorded as unverified: a name typed by hand is not corroborated by any register.
</div>
</div>
{cin.trim().length >= 21 && parsed && !parsed.valid && (
<ErrorNote message={parsed.error} />
)}
{error && <div style={{ marginTop: 12 }}><ErrorNote message={error} /></div>}
<button className="btn btn-primary" onClick={start}
disabled={!parsed?.valid || starting}
style={{ width: "100%", justifyContent: "center", marginTop: 6 }}>
{starting ? <><Spinner /> Building ledger</>
: <>Build disclosure ledger <ArrowRight size={15} /></>}
</button>
</div>
</section>
<aside style={{ display: "grid", gap: 14 }}>
{parsed?.valid ? (
<>
<section className="panel">
<div className="panel-h"><h2>What the identifier encodes</h2></div>
<div className="panel-b" style={{ display: "grid", gap: 10 }}>
{seg("Listing", parsed.is_listed ? "Listed" : "Unlisted",
`letter ${parsed.listing}, character 1`)}
{seg("Industry", `${parsed.industry_code}`,
parsed.nic_division_name)}
{seg("Registrar", parsed.state, `code ${parsed.state_code}`)}
{seg("Incorporated", String(parsed.year ?? ""),
`${parsed.age_years} year(s) ago`)}
{seg("Class", parsed.ownership, `code ${parsed.ownership_code}`)}
{seg("Registration", parsed.registration_number, "with the Registrar")}
</div>
</section>
{parsed.is_public_company === false && (
<Note tone="warn">
<b>A private company cannot make a public issue.</b> The engine will report
this as a blocker: conversion to a public limited company under the Companies
Act is a prerequisite, not a drafting fix.
</Note>
)}
{parsed.year !== null && new Date().getFullYear() - parsed.year < 3 && (
<Note tone="warn">
<b>Incorporated {parsed.year}.</b> Regulation 229(6) tests operating profit in
two of the last three financial years. The engine will report whether enough
financial years can have closed.
</Note>
)}
</>
) : (
<div className="panel">
<div className="panel-b">
<div style={{ fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.6 }}>
Enter a Corporate Identity Number to see what it encodes. Every one of its
21 characters means something.
</div>
<pre className="mono" style={{
fontSize: 11, lineHeight: 1.7, color: "var(--text-3)",
marginTop: 12, whiteSpace: "pre-wrap",
}}>{`U 62090 KA 2025 PTC 207753
β”‚ β”‚ β”‚ β”‚ β”‚ β”” registration no.
β”‚ β”‚ β”‚ β”‚ β”” class of company
β”‚ β”‚ β”‚ β”” year of incorporation
β”‚ β”‚ β”” state of the registrar
β”‚ β”” NIC industry code
β”” L listed, U unlisted`}</pre>
</div>
</div>
)}
</aside>
</div>
);
}