LoanCommitteeQueue
Loan applications before the credit committee: member, amount, purpose, the votes so far, where each stands, and approve or decline per row.
Demo
Awaiting the credit committee
3 awaiting a decision · 2 of 3 approvals carry a loan
| Member | Amount | Purpose | Votes | Status | Your vote |
|---|---|---|---|---|---|
| Ana MercadoM-011952 · LN-2026-0142Applied | ₱150,000.0024 months | Sari-sari store inventoryBusiness loanTwo co-makers | 1 approve, 0 decline, 2 to voteApproved by R. Esguerra | Pending1 more approval needed | |
| Benjie DalisayM-002311 · LN-2026-0147Applied | ₱85,000.508 months | Rice planting, 2 hectaresProduction loan | 1 approve, 1 decline, 1 to voteApproved by R. EsguerraDeclined by G. Tan-Molina | Pending1 more approval needed | You voted to approve |
| Marites AbadM-007840 · LN-2026-0151Applied | ₱40,000.0012 months | Tuition, second semesterSalary loan | 2 approve, 0 decline, 1 to voteApproved by R. Esguerra, G. Tan-Molina | Approved | Decided |
| Jun Paolo ReyesM-012004 · LN-2026-0153Applied | ₱600,000.0060 months | House constructionHousing loanOver the committee's limit: to the board | 0 approve, 2 decline, 1 to voteDeclined by R. Esguerra, G. Tan-Molina | Declined | You voted to decline |
| Lorna BautistaM-009377 · LN-2026-0156Applied | ₱25,000.006 months | Hospital billsEmergency loan | 0 approve, 0 decline, 3 to vote | Pending2 more approvals needed |
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/loan_committee_queue.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/loan_committee_queue.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/loan_committee_queue.qwe
view LoanCommitteeQueueDemo() = <LoanCommitteeQueue id="loan-committee-queue-demo" action="#committee-vote" committee={3} currency="PHP" hidden={{ meeting: "cc-2026-10-30" }} applications={[ { id: "LN-2026-0142", member: "Ana Mercado", member_number: "M-011952", amount: 150000, term: "24 months", product: "Business loan", purpose: "Sari-sari store inventory", note: "Two co-makers", applied: "2026-10-20", href: "#LN-2026-0142", votes: [{ name: "R. Esguerra", vote: "approve" }] }, { id: "LN-2026-0147", member: "Benjie Dalisay", member_number: "M-002311", amount: 85000.50, term: "8 months", product: "Production loan", purpose: "Rice planting, 2 hectares", applied: "2026-10-22", votes: [{ name: "R. Esguerra", vote: "approve" }, { name: "G. Tan-Molina", vote: "decline" }], voted: "approve" }, { id: "LN-2026-0151", member: "Marites Abad", member_number: "M-007840", amount: 40000, term: "12 months", product: "Salary loan", purpose: "Tuition, second semester", applied: "2026-10-23", votes: [{ name: "R. Esguerra", vote: "approve" }, { name: "G. Tan-Molina", vote: "approve" }] }, { id: "LN-2026-0153", member: "Jun Paolo Reyes", member_number: "M-012004", amount: 600000, term: "60 months", product: "Housing loan", purpose: "House construction", note: "Over the committee's limit: to the board", applied: "2026-10-24", votes: [{ name: "R. Esguerra", vote: "decline" }, { name: "G. Tan-Molina", vote: "decline" }], voted: "decline" }, { id: "LN-2026-0156", member: "Lorna Bautista", member_number: "M-009377", amount: 25000, term: "6 months", product: "Emergency loan", purpose: "Hospital bills", applied: "2026-10-28", votes: [] }, ]}/>Source
ui/loan_committee_queue.qwe
// LoanCommitteeQueue: the loan applications before a co-op's or credit union's// credit committee: who applied, for how much and what, the committee's votes so// far, where each stands, and Approve and Decline buttons on each row. Each row's// buttons are a plain <form method="post">: `application` (its id) and `decision`// ("approve" or "decline") go to `action`. An application is approved once// `needed` members approve, and declined once so many decline that it can no// longer be; the buttons go once it is decided, or once this member has voted.//// <LoanCommitteeQueue action="/committee/votes" committee={3} currency="PHP" applications={[// { id: "LN-2026-0142", member: "Ana Mercado", member_number: "M-011952", amount: 150000, term: "24 months",// purpose: "Sari-sari store inventory", applied: "2026-10-20",// votes: [{ name: "R. Esguerra", vote: "approve" }], voted: null },// ]}/>//// applications: maps of { id, member, member_number?, amount (a number or decimal),// term?, product? ("Regular loan"), purpose, note? (a line under the purpose: "Two// co-makers", "Over the committee's limit"), applied? (ISO date), href? (the// application's page), votes (the committee's so far: { name, vote: "approve" |// "decline" }), voted? (this member's own vote, "approve" or "decline", when they// have voted) }. committee: how many sit on the committee (3); needed: how many// approvals carry it (a majority by default). action: where the votes post// ("/committee/vote"); hidden: other fields each form sends, as { name: value }.// currency: ISO code ("USD"); locale: how to write amounts (the program's).// title: the heading ("Awaiting the credit committee"). id: the element's id.//// loan_committee_queue_status(votes, committee, needed) gives "approved", "declined"// or "pending", for a program's own use.import ui/theme.qwe use ui_clslet loan_committee_queue_css = css`.ui-loan-queue { display: grid; gap: 12px; min-width: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-loan-queue-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; }.ui-loan-queue .ui-loan-queue-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-loan-queue-sum { margin: 0; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-loan-queue-scroll { overflow-x: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); }.ui-loan-queue table { width: 100%; border-collapse: collapse; }.ui-loan-queue th { padding: 10px 12px; border-bottom: 1px solid var(--ui-border); background: var(--ui-subtle); color: var(--ui-muted); font-size: 12px; font-weight: 600; text-align: left; white-space: nowrap; }.ui-loan-queue td { padding: 12px; border-bottom: 1px solid var(--ui-border); vertical-align: top; }.ui-loan-queue tbody tr:last-child td { border-bottom: 0; }.ui-loan-queue-num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }.ui-loan-queue th.ui-loan-queue-num { text-align: right; }.ui-loan-queue-member { font-weight: 600; }.ui-loan-queue-member a { color: inherit; text-decoration: none; }.ui-loan-queue-member a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-loan-queue-member a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-loan-queue-sub { display: block; color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-loan-queue-id { font-family: var(--ui-mono); white-space: nowrap; }.ui-loan-queue-amount { font-weight: 600; }.ui-loan-queue-note { display: inline-block; margin-top: 4px; padding: 0 7px; border-radius: 999px; background: var(--ui-warning-soft); color: var(--ui-warning); font-size: 12px; font-weight: 500; }.ui-loan-queue-dots { display: flex; gap: 4px; margin: 2px 0 4px; }.ui-loan-queue-dot { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; border: 1.5px dashed var(--ui-border); font-size: 11px; font-weight: 700; line-height: 1; }.ui-loan-queue-yes { border: 0; background: var(--ui-success-soft); color: var(--ui-success); }.ui-loan-queue-no { border: 0; background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-loan-queue-who { color: var(--ui-muted); font-size: 12px; }.ui-loan-queue-status { display: inline-block; padding: 1px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap; background: var(--ui-info-soft); color: var(--ui-info); }.ui-loan-queue-approved .ui-loan-queue-status { background: var(--ui-success-soft); color: var(--ui-success); }.ui-loan-queue-declined .ui-loan-queue-status { background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-loan-queue-approved td, .ui-loan-queue-declined td { background: color-mix(in srgb, var(--ui-subtle) 50%, transparent); }.ui-loan-queue-form { display: flex; gap: 6px; margin: 0; justify-content: flex-end; }.ui-loan-queue-btn { height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 13px var(--ui-font); cursor: pointer; white-space: nowrap; }.ui-loan-queue-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-loan-queue-approve { border-color: transparent; background: var(--ui-success); color: #fff; }.ui-loan-queue-approve:hover { filter: brightness(1.08); }.ui-loan-queue-decline { color: var(--ui-danger); }.ui-loan-queue-decline:hover { border-color: var(--ui-danger); background: var(--ui-danger-soft); }.ui-loan-queue-mine { display: block; color: var(--ui-muted); font-size: 12px; text-align: right; white-space: nowrap; }.ui-loan-queue-empty { padding: 28px 12px; color: var(--ui-muted); text-align: center; }`// Where an application stands: "approved" once `needed` approve, "declined" once// too many decline for that, else "pending".fn loan_committee_queue_status(votes: list, committee: int, needed: int) -> str { let yes = len(votes | filter((v) => v.vote == "approve")) let no = len(votes | filter((v) => v.vote == "decline")) yes >= needed ? "approved" : (no > committee - needed ? "declined" : "pending")}// An amount in its currency; text as it is.fn loan_committee_queue_money(amount, currency: str, locale: str?) -> str { if type_of(amount) == "str" { return amount } let m = money.of(dec(amount), currency, "half_up") if is_err(m) { return str(amount) } locale != null ? money.format(m, locale) : money.format(m)}// An ISO date as "Oct 20, 2026"; other text as it is.fn loan_committee_queue_date(d, locale: str?) -> str = date.valid(str(d), "date") ? date.format(str(d), { locale: locale ?? "en-US", style: "medium" }) : str(d)view LoanCommitteeQueue(applications: list = [], committee: int = 3, needed: int? = null, action: str = "/committee/vote", hidden: map = {}, currency: str = "USD", locale: str? = null, title: str = "Awaiting the credit committee", id: str? = null, class: str? = null) { style global loan_committee_queue_css let need = needed ?? floor(committee / 2) + 1 let rows = applications | map((a) => set(a, "status", loan_committee_queue_status(a.votes ?? [], committee, need))) let open = len(rows | filter((r) => r.status == "pending")) let heading = id != null ? `${id}-title` : null <section class={ui_cls(["ui-loan-queue", class])} id={id} aria-labelledby={heading}> <div.ui-loan-queue-head> <h3.ui-loan-queue-title id={heading}>{title}</h3> <p.ui-loan-queue-sum>{open} awaiting a decision · {need} of {committee} approvals carry a loan</p> </div> <div.ui-loan-queue-scroll> <table> <thead><tr> <th scope="col">Member</th> <th.ui-loan-queue-num scope="col">Amount</th> <th scope="col">Purpose</th> <th scope="col">Votes</th> <th scope="col">Status</th> <th scope="col"><span.ui-sr>Your vote</span></th> </tr></thead> <tbody> {len(rows) == 0 && <tr><td.ui-loan-queue-empty colspan="6">No applications are waiting.</td></tr>} {for a in rows => { let votes = a.votes ?? [] let yes = votes | filter((v) => v.vote == "approve") let no = votes | filter((v) => v.vote == "decline") let waiting = committee - len(yes) - len(no) let marks = [...(yes | map((v) => "yes")), ...(no | map((v) => "no")), ...(range(waiting > 0 ? waiting : 0) | map((i) => "wait"))] let said = `${a.member}, ${a.id}` <tr key={a.id} class={`ui-loan-queue-row ui-loan-queue-${a.status}`} data-id={a.id}> <td> <span.ui-loan-queue-member>{a.href != null ? <a href={a.href}>{a.member}</a> : a.member}</span> <span.ui-loan-queue-sub>{a.member_number != null && `${a.member_number} · `}<span.ui-loan-queue-id>{a.id}</span></span> {a.applied != null && <span.ui-loan-queue-sub>Applied <time datetime={str(a.applied)}>{loan_committee_queue_date(a.applied, locale)}</time></span>} </td> <td.ui-loan-queue-num> <span.ui-loan-queue-amount>{loan_committee_queue_money(a.amount, currency, locale)}</span> {a.term != null && <span.ui-loan-queue-sub>{a.term}</span>} </td> <td> {a.purpose} {a.product != null && <span.ui-loan-queue-sub>{a.product}</span>} {a.note != null && <span.ui-loan-queue-note>{a.note}</span>} </td> <td.ui-loan-queue-votes> <span.ui-loan-queue-dots aria-hidden="true"> {for [i, m] in enumerate(marks) => <span key={i} class={`ui-loan-queue-dot ui-loan-queue-${m}`}>{m == "yes" ? "✓" : (m == "no" ? "✕" : "")}</span>} </span> <span.ui-loan-queue-who> {len(yes)} approve, {len(no)} decline{waiting > 0 && `, ${waiting} to vote`} </span> {len(yes) > 0 && <span.ui-loan-queue-sub.ui-loan-queue-names>Approved by {join(yes | map((v) => v.name), ", ")}</span>} {len(no) > 0 && <span.ui-loan-queue-sub.ui-loan-queue-names>Declined by {join(no | map((v) => v.name), ", ")}</span>} </td> <td> <span.ui-loan-queue-status>{a.status == "approved" ? "Approved" : (a.status == "declined" ? "Declined" : "Pending")}</span> {a.status == "pending" && <span.ui-loan-queue-sub>{need - len(yes)} more {need - len(yes) == 1 ? "approval" : "approvals"} needed</span>} </td> <td> {a.status != "pending" ? <span.ui-loan-queue-mine>{a.voted != null ? `You voted to ${a.voted}` : "Decided"}</span> : a.voted != null ? <span.ui-loan-queue-mine>You voted to {a.voted}</span> : <form.ui-loan-queue-form method="post" action={action}> {for k in keys(hidden) => <input type="hidden" name={k} value={str(hidden[k])}/>} <input type="hidden" name="application" value={a.id}/> <button.ui-loan-queue-btn.ui-loan-queue-approve type="submit" name="decision" value="approve" aria-label={`Approve ${said}`}>Approve</button> <button.ui-loan-queue-btn.ui-loan-queue-decline type="submit" name="decision" value="decline" aria-label={`Decline ${said}`}>Decline</button> </form>} </td> </tr> }} </tbody> </table> </div> </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 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 downloadsRepaymentScheduleA loan's installments, past and to come: paid, due or overdue with the days late and the penalty, and a pay button on the next one.8 views · 7 downloadsShareCapitalLedgerA member's share capital contributions with a running balance, and the progress toward the share capital the co-op requires.6 views · 5 downloadsAccountCardA bank account at a glance: type, nickname, masked number, current and available balance, what is on hold, and quick actions.8 views · 7 downloadsAssemblyNoticeA general assembly notice: date and time, the venue or online link, the quorum, a numbered agenda and an RSVP form.6 views · 6 downloads
Thread
Thread 0 posts
On LoanCommitteeQueue: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.