Finance

LoanCommitteeQueue

Loan applications before the credit committee: member, amount, purpose, the votes so far, where each stands, and approve or decline per row.

#finance#coop#loans#table#dashboard#no-js

Demo

Preview

Awaiting the credit committee

3 awaiting a decision · 2 of 3 approvals carry a loan

MemberAmountPurposeVotesStatusYour vote
Ana MercadoM-011952 · LN-2026-0142Applied ₱150,000.0024 monthsSari-sari store inventoryBusiness loanTwo co-makers1 approve, 0 decline, 2 to voteApproved by R. EsguerraPending1 more approval needed
Benjie DalisayM-002311 · LN-2026-0147Applied ₱85,000.508 monthsRice planting, 2 hectaresProduction loan1 approve, 1 decline, 1 to voteApproved by R. EsguerraDeclined by G. Tan-MolinaPending1 more approval neededYou voted to approve
Marites AbadM-007840 · LN-2026-0151Applied ₱40,000.0012 monthsTuition, second semesterSalary loan2 approve, 0 decline, 1 to voteApproved by R. Esguerra, G. Tan-MolinaApprovedDecided
Jun Paolo ReyesM-012004 · LN-2026-0153Applied ₱600,000.0060 monthsHouse constructionHousing loanOver the committee's limit: to the board0 approve, 2 decline, 1 to voteDeclined by R. Esguerra, G. Tan-MolinaDeclinedYou voted to decline
Lorna BautistaM-009377 · LN-2026-0156Applied ₱25,000.006 monthsHospital billsEmergency loan0 approve, 0 decline, 3 to votePending2 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.qwe
main.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>}
Thread
710Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.