Loan contract detail · Vektos
Tasks / Loan contract detail Detail page of a personal loan: status, key terms, amount still owed, repayment schedule and signed documents.
The brief What the agent reads, word for word.
What a good answer has The page must show these texts, or the run is not done. A good answer uses these components.
Best and worst runs By design-system violations.
By context and model Mean design-system violations per run on this task.
The reference answer Written by hand, graded with zero violations, rendered by the browser check in every build. The agent never sees it.
13 TableHeader,
14 TableRow,
15 } from "@vektos/ds";
16 import { css } from "styled-system/css";
17 import data from "./fixture.json";
18
19 const { contract, schedule, documents } = data;
20
21 const money = new Intl.NumberFormat("en-IE", { style: "currency", currency: contract.currency });
22 const percent = new Intl.NumberFormat("en-IE", { style: "percent", minimumFractionDigits: 2 });
23 const date = new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeZone: "UTC" });
24
25 interface Status {
26 tone: NonNullable<BadgeProps["tone"]>;
27 label: string;
28 }
29
30 const statusBadge: Record<string, Status> = {
31 active: { tone: "success", label: "Active" },
32 closed: { tone: "neutral", label: "Closed" },
33 overdue: { tone: "danger", label: "Overdue" },
34 };
35
36 const instalmentBadge: Record<string, Status> = {
37 paid: { tone: "success", label: "Paid" },
38 due: { tone: "warning", label: "Due" },
39 upcoming: { tone: "neutral", label: "Upcoming" },
40 };
41
42 // Unknown statuses from the API still render, as a neutral badge with the raw value.
43 const badgeFor = (map: Record<string, Status>, status: string): Status =>
44 map[status] ?? { tone: "neutral", label: status };
45
46 const terms = [
47 ["Amount borrowed", money.format(contract.principal)],
48 ["Nominal rate", percent.format(contract.nominalRate)],
49 ["APR", percent.format(contract.apr)],
50 ["Term", `${contract.termMonths} months`],
51 ["Monthly payment", money.format(contract.monthlyPayment)],
52 ["Start date", date.format(new Date(contract.startDate))],
53 ["Still owed", money.format(contract.outstanding)],
54 ] as const;
55
56 const stack = css({ display: "flex", flexDirection: "column", gap: "3" });
57 const heading = css({ fontSize: "md", fontWeight: "semibold" });
58
59 export default function LoanContractPage() {
60 const status = badgeFor(statusBadge, contract.status);
61
62 return (
63 <main className={css({ display: "flex", flexDirection: "column", gap: "6", p: "6" })}>
64 <Card>
65 <CardHeader>
66 <div className={css({ display: "flex", alignItems: "center", gap: "3" })}>
67 <CardTitle asChild>
68 <h1>{contract.product}</h1>
69 </CardTitle>
70 <Badge tone={status.tone}>{status.label}</Badge>
71 </div>
72 <CardDescription>
73 Contract {contract.number}, {contract.borrower}
74 </CardDescription>
75 </CardHeader>
76 <CardBody>
77 <dl className={css({ display: "flex", flexWrap: "wrap", gap: "6" })}>
78 {terms.map(([term, value]) => (
79 <div
80 key={term}
81 className={css({ display: "flex", flexDirection: "column", gap: "1" })}
82 >
83 <dt className={css({ color: "fg.muted", fontSize: "sm" })}>{term}</dt>
84 <dd className={css({ fontWeight: "semibold" })}>{value}</dd>
85 </div>
86 ))}
87 </dl>
88 </CardBody>
89 </Card>
90
91 <section className={stack}>
92 <h2 className={heading}>Repayment schedule</h2>
93 <Table aria-label="Repayment schedule">
94 <TableHead>
95 <TableRow>
96 <TableHeader numeric>No.</TableHeader>
97 <TableHeader>Due date</TableHeader>
98 <TableHeader numeric>Payment</TableHeader>
99 <TableHeader numeric>Principal</TableHeader>
100 <TableHeader numeric>Interest</TableHeader>
101 <TableHeader numeric>Balance after</TableHeader>
102 <TableHeader>Status</TableHeader>
103 </TableRow>
104 </TableHead>
105 <TableBody>
106 {schedule.map((row) => {
107 const badge = badgeFor(instalmentBadge, row.status);
108 return (
109 <TableRow key={row.number}>
110 <TableCell numeric>{row.number}</TableCell>
111 <TableCell>{date.format(new Date(row.dueDate))}</TableCell>
112 <TableCell numeric>{money.format(row.payment)}</TableCell>
113 <TableCell numeric>{money.format(row.principal)}</TableCell>
114 <TableCell numeric>{money.format(row.interest)}</TableCell>
115 <TableCell numeric>{money.format(row.remaining)}</TableCell>
116 <TableCell>
117 <Badge tone={badge.tone}>{badge.label}</Badge>
118 </TableCell>
119 </TableRow>
120 );
121 })}
122 </TableBody>
123 </Table>
124 </section>
125
126 <section className={stack}>
127 <h2 className={heading}>Signed documents</h2>
128 <Card>
129 <CardBody>
130 <ul className={stack}>
131 {documents.map((doc) => (
132 <li
133 key={doc.name}
134 className={css({ display: "flex", justifyContent: "space-between", gap: "4" })}
135 >
136 <span>{doc.name}</span>
137 <span className={css({ color: "fg.muted" })}>
138 Signed {date.format(new Date(doc.signedAt))}
139 </span>
140 </li>
141 ))}
142 </ul>
143 </CardBody>
144 </Card>
145 </section>
146 </main>
147 );
148 }
149