Finance

SavingsGoal

A savings goal: target and saved so far, the progress, the deposit needed each month to reach it by the date, and the last deposits.

#finance#coop#banking#card#calculator#no-js

Demo

Preview

New roof

54%
₱32,450.75of ₱60,000.00 by

₱3,443.66 a month for 8 months reaches it by Jun 30, 2027. ₱27,549.25 to go.

Last deposits

  • Salary deduction+₱3,500.00
  • Salary deduction+₱3,500.00
  • Cash, Poblacion branch+₱1,200.75
  • Salary deduction+₱3,500.00
  • Salary deduction+₱3,500.00
Add money

School fees, second semester

81%
₱6,500.00of ₱8,000.00 by

₱1,500.00 a month for 1 month reaches it by Nov 15, 2026. ₱1,500.00 to go.

Last deposits

  • +₱1,500.00

Emergency fund

100%
$1,620.00of $1,500.00 by

Goal reached. Well saved!

Tricycle down payment

91%
₱18,200.00of ₱20,000.00 by

The date has passed, ₱1,800.00 short. Set a new date to keep saving.

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/savings_goal.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/savings_goal.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/savings_goal.qwe
view SavingsGoalDemo() =    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px">        <SavingsGoal id="savings-goal-demo" title="New roof" target={60000} saved={32450.75} by="2027-06-30" today="2026-10-29"                     currency="PHP" account="Goal savings •••• 4821" href="#deposit"                     deposits={[                         { date: "2026-10-15", amount: 3500, note: "Salary deduction" },                         { date: "2026-09-30", amount: 3500, note: "Salary deduction" },                         { date: "2026-09-12", amount: 1200.75, note: "Cash, Poblacion branch" },                         { date: "2026-08-31", amount: 3500, note: "Salary deduction" },                         { date: "2026-07-31", amount: 3500, note: "Salary deduction" },                         { date: "2026-06-30", amount: 3500, note: "Salary deduction" },                     ]}/>        <SavingsGoal id="savings-goal-demo-school" title="School fees, second semester" target={8000} saved={6500} by="2026-11-15"                     today="2026-10-29" currency="PHP" deposits={[{ date: "2026-10-01", amount: 1500 }]}/>        <SavingsGoal id="savings-goal-demo-done" title="Emergency fund" target={1500} saved={1620} by="2026-12-31" today="2026-10-29"                     account="Share savings •••• 0377" href="#deposit"/>        <SavingsGoal id="savings-goal-demo-late" title="Tricycle down payment" target={20000} saved={18200} by="2026-09-30" today="2026-10-29"                     currency="PHP"/>    </div>

Source

ui/savings_goal.qwe
// SavingsGoal: a member's savings goal: what it is for, the target and what is// saved so far, the progress, what must be put in each month to reach it by the// date, and the last deposits. Worked out here, exactly: what is left divided by// the whole months to the date (at least one while the date is ahead), rounded up// to the currency's cent so the goal is met.////   <SavingsGoal title="New roof" target={60000} saved={32000} by="2027-06-30" currency="PHP"//                account="Goal savings •••• 4821" deposits={[{ date: "2026-10-15", amount: 2500 }, ...]}/>//// title: what it is for. target: the amount to reach; saved: what is in it now// (numbers or decimals, in `currency`, ISO code, "USD", written for `locale`, the// program's). by: the date to reach it by, ISO. today: the date to count from// (date.today() by default; give it to show a fixed day). deposits: the last// ones, newest first, each { date, amount, note? }; show: how many of them (5).// account: the account it is kept in, masked ("•••• 4821"). href: a link to add// money (add_label, "Add money"). id: the element's id.//// savings_goal_monthly(target, saved, by, today, currency) gives { left, months,// monthly } (money, an int, money; months 0 and monthly null when the date has// passed), for a program's own use.import ui/theme.qwe use ui_clslet savings_goal_css = css`.ui-savings-goal { display: grid; gap: 14px; align-content: start; min-width: 0; max-width: 440px; padding: 18px 20px; 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-savings-goal-head { display: flex; justify-content: space-between; align-items: start; gap: 12px; }.ui-savings-goal .ui-savings-goal-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-savings-goal-account { margin: 2px 0 0; color: var(--ui-muted); font-size: 12px; }.ui-savings-goal-pct { flex: none; padding: 2px 9px; border-radius: 999px; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 13px; font-weight: 650; font-variant-numeric: tabular-nums; }.ui-savings-goal-done .ui-savings-goal-pct { background: var(--ui-success-soft); color: var(--ui-success); }.ui-savings-goal-late .ui-savings-goal-pct { background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-savings-goal-amounts { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }.ui-savings-goal-saved { font-size: 26px; font-weight: 650; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }.ui-savings-goal-target { color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-savings-goal-bar { height: 10px; border-radius: 999px; background: var(--ui-subtle); overflow: hidden; }.ui-savings-goal-fill { height: 100%; border-radius: inherit; background: var(--ui-accent); }.ui-savings-goal-done .ui-savings-goal-fill { background: var(--ui-success); }.ui-savings-goal-late .ui-savings-goal-fill { background: var(--ui-danger); }.ui-savings-goal-plan { margin: 0; padding: 10px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-savings-goal-plan b { font-variant-numeric: tabular-nums; }.ui-savings-goal-done .ui-savings-goal-plan { background: var(--ui-success-soft); color: var(--ui-success); font-weight: 500; }.ui-savings-goal-late .ui-savings-goal-plan { background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-savings-goal-recent h4 { margin: 0 0 4px; color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-savings-goal-recent ul { margin: 0; padding: 0; list-style: none; }.ui-savings-goal-recent li { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--ui-border); }.ui-savings-goal-recent li:last-child { border-bottom: 0; }.ui-savings-goal-recent time { color: var(--ui-muted); }.ui-savings-goal-note { color: var(--ui-muted); font-size: 12px; margin-left: 6px; }.ui-savings-goal-amount { font-weight: 600; color: var(--ui-success); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-savings-goal-add { justify-self: start; display: inline-flex; align-items: center; height: 36px; padding: 0 16px; border-radius: var(--ui-radius-sm);  background: var(--ui-accent); color: var(--ui-accent-fg); font-weight: 500; text-decoration: none; }.ui-savings-goal-add:hover { filter: brightness(1.08); }.ui-savings-goal-add:focus-visible { outline: none; box-shadow: var(--ui-focus); }`// What is left, the whole months to `by` (at least 1 while it is ahead; 0 once it// has passed) and the deposit each month, rounded up to the cent (null when passed).fn savings_goal_monthly(target, saved, by: str, today: str, currency: str) {    let t = money.of(dec(target), currency, "half_up")    let s = money.of(dec(saved), currency, "half_up")    let left = money.compare(s, t) >= 0 ? money.zero(currency) : money.sub(t, s)    let ahead = by >= today    let months = ahead ? max([date.months_between(today, by), 1]) : 0    let monthly = months == 0 ? null : money.of(round(left.amount / months, money.minor_units(currency), "ceiling"), currency)    { left, months, monthly }}// An ISO date as "Jun 30, 2027"; other text as it is.fn savings_goal_date(d, locale: str?) -> str =    date.valid(str(d), "date") ? date.format(str(d), { locale: locale ?? "en-US", style: "medium" }) : str(d)view SavingsGoal(title: str, target, saved = 0, by: str, today: str? = null, deposits: list = [], show: int = 5,                 currency: str = "USD", locale: str? = null, account: str? = null, href: str? = null, add_label: str = "Add money",                 id: str? = null, class: str? = null) {    style global savings_goal_css    let from = today ?? date.today()    let plan = savings_goal_monthly(target, saved, by, from, currency)    let shown = (m) => locale != null ? money.format(m, locale) : money.format(m)    let amount = (x) => shown(money.of(dec(x), currency, "half_up"))    let t = dec(target)    let s = dec(saved)    let pct = t <= 0 ? 100 : (s >= t ? 100 : (s <= 0 ? 0 : int(floor(s * 100 / t))))    let done = s >= t    let late = !done && plan.months == 0    let mood = done ? "done" : late ? "late" : null    let when = savings_goal_date(by, locale)    <section class={ui_cls(["ui-savings-goal", mood != null && `ui-savings-goal-${mood}`, class])} id={id} aria-label={`Savings goal: ${title}`}>        <div.ui-savings-goal-head>            <div>                <h3.ui-savings-goal-title>{title}</h3>                {account != null && <p.ui-savings-goal-account>{account}</p>}            </div>            <span.ui-savings-goal-pct>{pct}%</span>        </div>        <div.ui-savings-goal-amounts>            <span.ui-savings-goal-saved>{amount(saved)}</span>            <span.ui-savings-goal-target>of {amount(target)} by <time datetime={by}>{when}</time></span>        </div>        <div.ui-savings-goal-bar role="progressbar" aria-label={`${title}: saved so far`} aria-valuemin="0" aria-valuemax="100" aria-valuenow={pct}             aria-valuetext={`${pct}%, ${amount(saved)} of ${amount(target)}`}>            <div.ui-savings-goal-fill style={`width: ${pct}%`}></div>        </div>        <p.ui-savings-goal-plan>            {done ? "Goal reached. Well saved!"                  : late ? [`The date has passed, ${shown(plan.left)} short. Set a new date to keep saving.`]                  : [<b>{shown(plan.monthly)}</b>, " a month for ", <b>{plan.months} {plan.months == 1 ? "month" : "months"}</b>,                     ` reaches it by ${when}. ${shown(plan.left)} to go.`]}        </p>        {len(deposits) > 0 && <div.ui-savings-goal-recent>            <h4>Last deposits</h4>            <ul>                {for [i, d] in enumerate(slice(deposits, 0, show)) => <li key={i}>                    <span><time datetime={str(d.date)}>{savings_goal_date(d.date, locale)}</time>{d.note != null && <span.ui-savings-goal-note>{d.note}</span>}</span>                    <span.ui-savings-goal-amount>+{amount(d.amount)}</span>                </li>}            </ul>        </div>}        {href != null && !done && <a.ui-savings-goal-add href={href}>{add_label}</a>}    </section>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.