HDFC-Loan-Desk/src/pages/Login.tsx
Yashas 2e8cc580ac feat: HDFC Loan Desk operator console
Custom frontend for the HDFC loan-origination demo (dev, org 83 / app 524,
workflow hdfc_wf_loan). MSME and personal loan files: agents assemble the
evidence, rules compute capacity, credit decides.

Stack and platform contract follow the Flight-Disruption-Management console,
which runs against this same cluster:

  * API client carries its predecessor's hard-won notes — instance_id goes
    over the wire as a NUMBER (a quoted id fails the int64 decode), org_id as
    a STRING, record views answer under `data` OR `records`, and audit rows
    need the TRIGGER_ prefix filtered or every activity appears three times.
  * VITE_ZINO_API_URL is read at RUNTIME from the config.js the server writes
    at placement, never compiled in, so one artifact is promoted between
    environments unchanged. Missing config fails loudly.
  * base: './' plus a router basename from <base href>, so one build serves
    any mount path.
  * Forms are read from the LIVE activity schema — no field definitions in
    this repo. Add a field in Studio, redeploy, it appears.

Written for this app:

  * EvidencePanel, the centrepiece. Three independent income sources side by
    side with the widest pair marked; every computed ratio shown against the
    threshold it was tested on; and a visible line between what a rule
    computed and what a model wrote (rules are never violet).
  * Queues are the one record view filtered server-side on
    current_state_name — the sidebar is the pipeline. Income variance is
    surfaced in the list, not only on the file.
  * Application 360 with the evidence panel above the offer and the sanction,
    so the screen reads in the order the decision was made.
  * HDFC palette where red is never decoration: the logo block and declines
    only. The referred queue's amber is the only amber in the pipeline.

Known gaps, documented in README rather than hidden: OCR uploads render as a
visible pending row instead of a control that pretends to work, and Credit
Assessment is still performed by a human pending the agent wiring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:42:55 +05:30

78 lines
2.6 KiB
TypeScript

import { useState } from 'react';
import { useZino } from '../api/provider';
import { ORG_ID } from '../api/config';
import { Button, ErrorNote, Input, Label } from '../components/core';
import { HdfcWordmark } from '../components/HdfcMark';
import { ThemeToggle } from '../components/ThemeToggle';
/**
* Sign in.
*
* The org is fixed — this build serves one tenant — so it is not a field the
* user has to know. It still goes over the wire as a STRING: the gateway
* rejects a number with "cannot unmarshal number into Go struct field
* LoginRequest.org_id", which is a confusing failure to hit from a login form.
*/
export function Login() {
const { login } = useZino();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [err, setErr] = useState<string | null>(null);
async function submit() {
if (!email || !password) {
setErr('Enter your email and password');
return;
}
setBusy(true);
setErr(null);
try {
await login(email, password, ORG_ID);
} catch (e) {
setErr((e as { message?: string })?.message ?? 'Could not sign in');
} finally {
setBusy(false);
}
}
return (
<div className="flex min-h-full flex-col bg-bg">
<header className="flex items-center justify-between border-b border-deepnavy bg-navy px-4 py-2.5">
<HdfcWordmark onNavy />
<ThemeToggle onNavy />
</header>
<div className="flex flex-1 items-center justify-center p-4">
<form
onSubmit={(e) => {
e.preventDefault();
void submit();
}}
className="lift w-full max-w-sm rounded-lg border border-line bg-panel p-6"
>
<h1 className="text-lg font-semibold text-ink">Sign in</h1>
<p className="mt-1 text-sm text-muted">
MSME and personal loan origination.
</p>
<div className="mt-5 space-y-3">
{err && <ErrorNote>{err}</ErrorNote>}
<div>
<Label required>Email</Label>
<Input value={email} onChange={setEmail} type="email" placeholder="name@hdfc.example" />
</div>
<div>
<Label required>Password</Label>
<Input value={password} onChange={setPassword} type="password" placeholder="••••••••" />
</div>
<Button kind="primary" type="submit" busy={busy} className="w-full">
Sign in
</Button>
</div>
</form>
</div>
</div>
);
}