Payment approval · Vektos
Tasks / Payment approval List of payments waiting for approval; each opens a confirmation dialog with amount, fee and total, and is marked approved after confirming.
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 TableCell,
14 TableHead,
15 TableHeader,
16 TableRow,
17 } from "@vektos/ds";
18 import { useState } from "react";
19 import { css } from "styled-system/css";
20 import data from "./fixture.json";
21
22 type Payment = (typeof data.payments)[number];
23
24 const money = new Intl.NumberFormat("en-IE", { style: "currency", currency: data.currency });
25 const date = new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeZone: "UTC" });
26
27 const fees: Record<string, number> = data.fees;
28 const feeFor = (payment: Payment) => fees[payment.scheme] ?? 0;
29
30 function Summary({ payment }: { payment: Payment }) {
31 const fee = feeFor(payment);
32 const rows = [
33 ["Beneficiary", payment.beneficiary],
34 ["IBAN", payment.iban],
35 ["Amount", money.format(payment.amount)],
36 [`Fee (${payment.scheme === "instant" ? "instant" : "SEPA"})`, money.format(fee)],
37 ] as const;
38
39 return (
40 <dl className={css({ display: "flex", flexDirection: "column", gap: "2" })}>
41 {rows.map(([term, value]) => (
42 <div
43 key={term}
44 className={css({ display: "flex", justifyContent: "space-between", gap: "4" })}
45 >
46 <dt className={css({ color: "fg.muted" })}>{term}</dt>
47 <dd>{value}</dd>
48 </div>
49 ))}
50 <div
51 className={css({
52 display: "flex",
53 justifyContent: "space-between",
54 gap: "4",
55 paddingBlockStart: "2",
56 borderBlockStartWidth: "thin",
57 borderColor: "border.default",
58 fontWeight: "semibold",
59 })}
60 >
61 <dt>Total</dt>
62 <dd>{money.format(payment.amount + fee)}</dd>
63 </div>
64 </dl>
65 );
66 }
67
68 export default function PaymentApprovalPage() {
69 const [approved, setApproved] = useState<ReadonlySet<string>>(new Set());
70
71 const approve = (id: string) => setApproved((current) => new Set(current).add(id));
72
73 return (
74 <main className={css({ display: "flex", flexDirection: "column", gap: "4", p: "6" })}>
75 <h1 className={css({ fontSize: "lg", fontWeight: "semibold" })}>Payments to approve</h1>
76 <Table aria-label="Payments to approve">
77 <TableHead>
78 <TableRow>
79 <TableHeader>Beneficiary</TableHeader>
80 <TableHeader>IBAN</TableHeader>
81 <TableHeader>Due</TableHeader>
82 <TableHeader numeric>Amount</TableHeader>
83 <TableHeader>
84 <span className={css({ srOnly: true })}>Action</span>
85 </TableHeader>
86 </TableRow>
87 </TableHead>
88 <TableBody>
89 {data.payments.map((payment) => (
90 <TableRow key={payment.id}>
91 <TableCell>{payment.beneficiary}</TableCell>
92 <TableCell>{payment.iban}</TableCell>
93 <TableCell>{date.format(new Date(payment.dueDate))}</TableCell>
94 <TableCell numeric>{money.format(payment.amount)}</TableCell>
95 <TableCell>
96 {approved.has(payment.id) ? (
97 <Badge tone="success">Approved</Badge>
98 ) : (
99 <Dialog>
100 <DialogTrigger asChild>
101 <Button
102 variant="outline"
103 size="sm"
104 aria-label={`Approve payment to ${payment.beneficiary}`}
105 >
106 Approve
107 </Button>
108 </DialogTrigger>
109 <DialogContent title="Approve payment" description={payment.reference}>
110 <Summary payment={payment} />
111 <DialogFooter>
112 <DialogClose asChild>
113 <Button variant="outline">Cancel</Button>
114 </DialogClose>
115 <DialogClose asChild>
116 <Button onClick={() => approve(payment.id)}>Confirm</Button>
117 </DialogClose>
118 </DialogFooter>
119 </DialogContent>
120 </Dialog>
121 )}
122 </TableCell>
123 </TableRow>
124 ))}
125 </TableBody>
126 </Table>
127 </main>
128 );
129 }
130