PaymentHistory
Twenty-four months of payments as a grid of years and months, on time, 30, 60 or 90 days late, or nothing due, as credit reports show them, with a legend.
Demo
Harbor Federal, home loan •••• 4821
95% on time: 23 of 24 payments due, over 24 months| Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026 | On time | On time | On time | On time | On time | On time | On time | On time | On time | On time | ||
| 2025 | On time | On time | On time | On time | On time | On time | 30 days late | On time | On time | On time | On time | On time |
| 2024 | On time | On time |
- On time 23
- 30 days late 1
- 60 days late 0
- 90 or more days late 0
- No payment due 0
Northwind Visa •••• 7710
75% on time: 15 of 20 payments due, over 24 months| Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026 | On time | On time | On time | On time | On time | Not reported | On time | On time | On time | On time | ||
| 2025 | On time | On time | 30 days late | 60 days late | 90 or more days late | 90 or more days late | On time | On time | On time | No payment due | On time | 30 days late |
| 2024 | No payment due | No payment due |
- On time 15
- 30 days late 2
- 60 days late 1
- 90 or more days late 2
- No payment due 3
- Not reported 1
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/payment_history.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/payment_history.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/payment_history.qwe
view PaymentHistoryDemo() { <div.demo-stack> <PaymentHistory id="payment-history-demo-home" account="Harbor Federal, home loan •••• 4821" start="2024-11" statuses={[ "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "30", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok", "ok"]}/> <PaymentHistory id="payment-history-demo-card" account="Northwind Visa •••• 7710" start="2024-11" statuses={[ "none", "none", "ok", "ok", "30", "60", "90", "120", "ok", "ok", "ok", "none", "ok", "30", "ok", "ok", "ok", "ok", "ok", null, "ok", "ok", "ok", "ok"]}/> </div>}Source
ui/payment_history.qwe
// PaymentHistory: an account's payments month by month, as credit reports show// them: a row a year, a column a month, each month on time, 30, 60 or 90+ days// late, or with no payment due, coloured and marked so the colour is not needed,// with a legend and the count of each under it. It is a table, so screen readers// read each month with its year and what happened.//// <PaymentHistory account="Harbor Federal, home loan •••• 4821" start="2024-11"// statuses={["ok", "ok", "30", "ok", "none", ...]}/>//// statuses: one per month, oldest first: "ok" (paid on time), "30", "60", "90"// (days late; "120" and over count as 90+), "none" (no payment due), or null or ""// (nothing reported). start: the first month, "YYYY-MM" (or a date). newest_first:// the latest year on top, as reports have it (true). account: a heading over it// (the lender and the account, masked). locale: the months' names (the program's;// en-US). summary: false leaves out the counts. label: the table's caption,// for screen readers ("Payment history"). id, class.import ui/theme.qwe use ui_clslet payment_history_css = css`.ui-pay-history { display: grid; gap: 12px; min-width: 0; padding: 16px 18px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-pay-history-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; }.ui-pay-history-title { margin: 0; font-size: 15px; font-weight: 650; }.ui-pay-history-rate { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-pay-history-rate b { color: var(--ui-fg); }.ui-pay-history-scroll { overflow-x: auto; }.ui-pay-history table { border-collapse: separate; border-spacing: 4px; margin: -4px; font-variant-numeric: tabular-nums; }.ui-pay-history th { padding: 0 2px; color: var(--ui-muted); font-size: 11px; font-weight: 600; text-align: center; }.ui-pay-history thead th abbr { text-decoration: none; }.ui-pay-history tbody th { padding-right: 8px; text-align: left; font-size: 12px; color: var(--ui-fg); }.ui-pay-history td { padding: 0; }.ui-pay-history-cell { display: grid; place-items: center; width: 34px; height: 28px; border-radius: 5px; font-size: 11px; font-weight: 700; background: var(--ui-subtle); color: var(--ui-muted); }.ui-pay-history-ok { background: var(--ui-success-soft); color: var(--ui-success); }.ui-pay-history-30 { background: color-mix(in srgb, var(--ui-warning) 22%, transparent); color: color-mix(in srgb, var(--ui-warning) 75%, var(--ui-fg)); }.ui-pay-history-60 { background: color-mix(in srgb, var(--ui-danger) 40%, var(--ui-warning)); color: #fff; }.ui-pay-history-90 { background: var(--ui-danger); color: #fff; }.ui-pay-history-none { background: transparent; box-shadow: inset 0 0 0 1px var(--ui-border); }.ui-pay-history-blank { background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-border) 55%, transparent); }.ui-pay-history-out { visibility: hidden; }.ui-pay-history-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 12px; }.ui-pay-history-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-pay-history-legend .ui-pay-history-cell { width: 26px; height: 20px; font-size: 10px; }.ui-pay-history-legend b { color: var(--ui-fg); font-weight: 650; font-variant-numeric: tabular-nums; }`let payment_history_kinds = [ { code: "ok", mark: "✓", said: "On time" }, { code: "30", mark: "30", said: "30 days late" }, { code: "60", mark: "60", said: "60 days late" }, { code: "90", mark: "90", said: "90 or more days late" }, { code: "none", mark: "–", said: "No payment due" }, { code: "blank", mark: "", said: "Not reported" },]// A status as one of the kinds' codes: "120" and over are "90"; nothing is "blank".fn payment_history_code(s) -> str { if s == null || str(s) == "" { return "blank" } let t = lower(str(s)) let n = int(t) if t == "ok" || t == "on_time" || t == "0" { return "ok" } if n != null && !is_err(n) { return n >= 90 ? "90" : (n >= 60 ? "60" : (n >= 30 ? "30" : "ok")) } t == "none" || t == "nd" ? "none" : "blank"}view PaymentHistory(statuses: list, start: str, newest_first: bool = true, account: str? = null, locale: str? = null, summary: bool = true, label: str = "Payment history", id: str? = null, class: str? = null) { style global payment_history_css let first = `${slice(start, 0, 7)}-01` let months = for [i, s] in enumerate(statuses) { let d = date.add_months(first, i) { ym: slice(d, 0, 7), year: slice(d, 0, 4), month: int(slice(d, 5, 7)), code: payment_history_code(s) } } let years_up = unique(for m in months => m.year) let years = newest_first ? reverse(years_up) : years_up let lang = locale ?? "en-US" let names = for k in range(12) => date.format(`2026-${k < 9 ? "0" : ""}${k + 1}-01`, { locale: lang, pattern: "MMM" }) let long_names = for k in range(12) => date.format(`2026-${k < 9 ? "0" : ""}${k + 1}-01`, { locale: lang, pattern: "MMMM" }) let tally = (code: str) => len(filter(months, (m) => m.code == code)) let due = len(filter(months, (m) => m.code != "none" && m.code != "blank")) let on_time = tally("ok") let pct = due > 0 ? int(floor(on_time * 100 / due)) : 100 <section class={ui_cls(["ui-pay-history", class])} id={id} aria-label={account ?? label}> <div.ui-pay-history-head> {account != null && <h3.ui-pay-history-title>{account}</h3>} {summary && <span.ui-pay-history-rate data-key="rate"><b>{pct}%</b> on time: {on_time} of {due} {due == 1 ? "payment" : "payments"} due, over {len(months)} months</span>} </div> <div.ui-pay-history-scroll> <table> <caption class="ui-sr">{label}{account != null ? `, ${account}` : ""}</caption> <thead><tr> <td></td> {for k in range(12) => <th scope="col" key={k}><abbr title={long_names[k]}>{names[k]}</abbr></th>} </tr></thead> <tbody> {for y in years => <tr key={y} data-year={y}> <th scope="row">{y}</th> {for k in range(12) { let m = find(months, (x) => x.year == y && x.month == k + 1) let kind = m != null ? find(payment_history_kinds, (x) => x.code == m.code) : null <td key={k} data-month={m != null ? m.ym : null} data-status={m != null ? m.code : null}> {m != null ? <span class={`ui-pay-history-cell ui-pay-history-${m.code}`} title={`${long_names[k]} ${y}: ${kind.said}`}> <span aria-hidden="true">{kind.mark}</span><span.ui-sr>{kind.said}</span> </span> : <span class="ui-pay-history-cell ui-pay-history-out" aria-hidden="true"></span>} </td> }} </tr>} </tbody> </table> </div> <ul.ui-pay-history-legend aria-label="Legend"> {for kind in payment_history_kinds { let n = tally(kind.code) (kind.code != "blank" || n > 0) && <li key={kind.code} data-code={kind.code}> <span class={`ui-pay-history-cell ui-pay-history-${kind.code}`} aria-hidden="true">{kind.mark}</span> {kind.said}{summary && [" ", <b>{n}</b>]} </li> }} </ul> </section>}Related
AmortizationTableA loan's payment schedule worked out from its amount, rate and term: each month's principal, interest and balance, grouped by year with the year's totals.7 views · 7 downloadsCollateralListWhat secures a loan: each item's valuation and documents, and the loan-to-value ratio as a meter against the lender's limit.7 views · 5 downloadsCreditScoreA credit score on a gauge of its bands from poor to excellent, the band it falls in, the change since the last report and the factors that move it.7 views · 8 downloadsLoanOffersLoan offers side by side: each lender's APR, term, monthly payment, fees and total cost, with the cheapest in total marked.8 views · 7 downloadsCrossRatesA matrix of cross rates among a few currencies, each worked out from their rates against one base, the diagonal left blank.6 views · 6 downloadsCurrencyPairCardOne currency pair: the bid and ask large with the pip digits biggest, the spread, the day's change, low and high, and a sparkline of the day.7 views · 6 downloads
Thread
Thread 0 posts
On PaymentHistory: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.