Finance

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.

#finance#loans#chart#data#no-js

Demo

Preview

Harbor Federal, home loan •••• 4821

95% on time: 23 of 24 payments due, over 24 months
Payment history, Harbor Federal, home loan •••• 4821
JanFebMarAprMayJunJulAugSepOctNovDec
2026On timeOn timeOn timeOn timeOn timeOn timeOn timeOn timeOn timeOn time
2025On timeOn timeOn timeOn timeOn timeOn time30 days lateOn timeOn timeOn timeOn timeOn time
2024On timeOn 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
Payment history, Northwind Visa •••• 7710
JanFebMarAprMayJunJulAugSepOctNovDec
2026On timeOn timeOn timeOn timeOn timeNot reportedOn timeOn timeOn timeOn time
2025On timeOn time30 days late60 days late90 or more days late90 or more days lateOn timeOn timeOn timeNo payment dueOn time30 days late
2024No payment dueNo 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.qwe
main.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>}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On PaymentHistory: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.