Commerce

MarginCalc

Cost, price, margin and markup bound together: edit any and the rest follow from the two set last, with a price for a target margin and a warning under cost.

#smart#interactive#commerce#calculator#margin#pricing

Demo

Preview

Margin and markup

Profit on each
$7.59
Margin on the price
37.97%
Markup on the cost
61.21%

Price for 45%: $22.55.

Wholesale, in euros

Profit on each
€26.67
Margin on the price
25.00%
Markup on the cost
33.34%

Give a target margin to see the price that reaches it.

Change a figure to see on_change.

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

Example

The code of the preview above.

demos/margin_calc.qwe
component MarginCalcDemoLive() {    state seen = null    <div.demo-stack>        <MarginCalc id="margin-calc-demo-live" title="Wholesale, in euros" currency="EUR" name="trade" cost="80" margin="25"                    on_change={(f) => { seen = f }}/>        <p.demo-note>{seen == null ? "Change a figure to see on_change." : `on_change: price ${seen.price}, profit ${seen.profit}.`}</p>    </div>}view MarginCalcDemo() =    <div.demo-stack>        <MarginCalc id="margin-calc-demo" cost="12.40" price="19.99" target={45}/>        <MarginCalcDemoLive/>    </div>

Source

ui/margin_calc.qwe
// MarginCalc: cost, price, margin and markup bound together. Edit any of them and// the rest follow from the two set last, which are marked, so changing the price// with the cost set moves the margin, and changing the margin with the price set// moves the cost. Margin and markup say the same thing, so they are never the// two: setting one after the other keeps the last amount set beside it. A target// margin gives the price that reaches it, rounded up so it is not missed, and a// price under cost says what each sale loses. Everything is a decimal, the amounts// rounded half up to the currency's cents, and the percentages are worked out// from those rounded amounts, so what shows adds up.////   <MarginCalc cost={12.40} price={19.99} target={45}/>//   <MarginCalc currency="EUR" cost="80" margin="25" on_change={(f) => { price = f.price }}/>//// cost, price, margin, markup: where the fields start (numbers, decimals or text,// margin and markup in percent); the first two given are the two set. target: the// target margin in percent, with its own field (left out, empty). currency: ISO// code ("USD"). locale: how to write amounts (the program's). title: the heading// ("Margin and markup"). id: a prefix for its fields' ids ("margin-calc"), unique// on the page. The fields are named cost, price, margin, markup and target_margin,// so a form around it sends them; name: a prefix for those names. on_change(figures):// inside one of your components, given { cost, price, margin, markup, profit } as// decimals (margin null at a price of 0, markup null at a cost of 0) after each// change that gives a whole answer.//// margin_calc_solve(a, b, values, minor) is the arithmetic, for a program's own use:// a and b name the two set of "cost", "price", "margin" and "markup", values holds// them as decimals and minor is the currency's decimals. It gives { cost, price,// margin, markup, profit, error }. margin_calc_target(cost, target, minor) is the// price for a target margin.import ui/theme.qwe use ui_clslet margin_calc_css = css`.ui-margincalc { display: grid; gap: 16px; min-width: 0; max-width: 560px; padding: 20px 22px; 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-margincalc .ui-margincalc-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-margincalc-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 14px; }.ui-margincalc-field { display: grid; gap: 5px; min-width: 0; }.ui-margincalc-field label { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 13px; font-weight: 500; }.ui-margincalc-tag { color: var(--ui-muted); font-size: 11px; font-weight: 500; }.ui-margincalc-set .ui-margincalc-tag { color: var(--ui-accent); font-weight: 650; }.ui-margincalc-box { box-sizing: border-box; display: flex; align-items: center; gap: 6px; min-width: 0; height: 38px; padding: 0 10px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); transition: border-color .15s, box-shadow .15s; }.ui-margincalc-set .ui-margincalc-box { background: var(--ui-subtle); }.ui-margincalc-box:hover { border-color: var(--ui-strong); }.ui-margincalc-box:focus-within { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-margincalc-box input { flex: 1; width: 0; min-width: 0; height: 100%; padding: 0; border: 0; outline: none; background: transparent;  color: var(--ui-fg); font: 14px var(--ui-font); font-variant-numeric: tabular-nums; text-align: right; }.ui-margincalc-box span { flex: none; color: var(--ui-muted); font-size: 12px; font-weight: 600; }.ui-margincalc-bad .ui-margincalc-box { border-color: var(--ui-danger); }.ui-margincalc-out { display: grid; margin: 0; padding: 4px 16px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-margincalc-out div { display: flex; justify-content: space-between; gap: 16px; padding: 8px 0; border-bottom: 1px solid var(--ui-border); }.ui-margincalc-out div:last-child { border-bottom: 0; }.ui-margincalc-out dt { font-weight: 500; }.ui-margincalc-out dd { margin: 0; text-align: right; font-weight: 650; font-variant-numeric: tabular-nums; }.ui-margincalc-target { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px 14px; align-items: end; }.ui-margincalc-use { min-height: 38px; 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; }.ui-margincalc-use:hover:not(:disabled) { border-color: var(--ui-accent); color: var(--ui-accent); }.ui-margincalc-use:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-margincalc-use:disabled { opacity: .55; cursor: not-allowed; }.ui-margincalc-say { grid-column: 1 / -1; margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-margincalc-note { margin: 0; color: var(--ui-muted); font-size: 13px; }.ui-margincalc-warn { margin: 0; padding: 8px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-warning-soft); color: var(--ui-warning); font-size: 13px; font-weight: 500; }.ui-margincalc-error { margin: 0; color: var(--ui-danger); font-size: 13px; }@media (max-width: 420px) { .ui-margincalc { padding: 16px; } .ui-margincalc-grid, .ui-margincalc-target { grid-template-columns: 1fr; } }@media (prefers-reduced-motion: reduce) { .ui-margincalc-box { transition: none; } }`let margin_calc_fields = [    { id: "cost", label: "Cost", ratio: false },    { id: "price", label: "Price", ratio: false },    { id: "margin", label: "Margin", ratio: true },    { id: "markup", label: "Markup", ratio: true }]// What was typed as a decimal, null when blank, or an error value. "1,234.50" is// read; "12,50" is not guessed at, and neither is the exponent dec() would take.fn margin_calc_read(text) {    let t = re.replace(trim(str(text ?? "")), "[\\s%]", "")    if t == "" { return null }    let plain = re.test(t, "^[+-]?\\d{1,3}(,\\d{3})+(\\.\\d*)?$") ? re.replace(t, ",", "") : t    if !re.test(plain, "^[+-]?(\\d+\\.?\\d*|\\.\\d+)$") { return err("is not a number") }    // Past this the arithmetic could overflow a decimal, and no price is that large.    if len(plain) > 20 { return err("is too large") }    let d = dec(plain)    if is_err(d) || abs(d) > 1000000000000m { return err("is too large") }    d}// The two fields set after `field` is edited: it and the one set before, but for// margin and markup, which only say one thing between them: then the amount stays.fn margin_calc_pair(field: str, a: str, b: str) {    let ratio = (f) => f == "margin" || f == "markup"    if field == a { return [a, b] }    if field == b { return [b, a] }    ratio(field) && ratio(a) ? [field, b] : [field, a]}// The currency's code, or USD for one money does not know, which would stop the sums.fn margin_calc_code(currency: str) -> str {    let code = upper(trim(currency))    is_err(money.minor_units(code)) ? "USD" : code}// The price that gives a margin in percent, exact but for a division's last digit.fn margin_calc_price_for(cost, margin) = cost * 100 / (100 - margin)// The whole answer from two values: see the top of the file.fn margin_calc_solve(a: str, b: str, values, minor: int) {    let given = (f) => a == f || b == f ? values[f] : null    let c = given("cost")    let p = given("price")    let m = given("margin")    let k = given("markup")    let out = (cost, price) => {        let rc = round(cost, minor, "half_up")        let rp = round(price, minor, "half_up")        { cost: rc, price: rp, profit: rp - rc, error: null,          margin: rp == 0 ? null : (rp - rc) * 100 / rp, markup: rc == 0 ? null : (rp - rc) * 100 / rc }    }    let wrong = (text) => ({ error: text })    if c != null && c < 0 { return wrong("A cost cannot be below 0.") }    if p != null && p < 0 { return wrong("A price cannot be below 0.") }    if c != null && p != null { return out(c, p) }    if c != null && m != null {        if m >= 100 {            return wrong(c == 0 ? "With a cost of 0 the margin is 100% at any price: set the price instead." : "A margin of 100% or more cannot be reached with a cost above 0.")        }        if m > 99.9999m { return wrong("That margin is too close to 100% to give a price.") }        return out(c, margin_calc_price_for(c, m))    }    if c != null && k != null {        if k < -100 { return wrong("A markup below -100% would need a price below 0.") }        return out(c, c * (100 + k) / 100)    }    if p != null && m != null {        if m > 100 { return wrong("A margin above 100% would need a cost below 0.") }        return out(p * (100 - m) / 100, p)    }    if p != null && k != null {        if k <= -100 { return wrong(p == 0 ? "With a price of 0 and that markup any cost fits: set the cost instead." : "A markup of -100% or less cannot give a price above 0.") }        return out(p * 100 / (100 + k), p)    }    null}// The price for a target margin, rounded up to the cent so the margin is reached.fn margin_calc_target(cost, target, minor: int) {    if target >= 100 { return err("A target of 100% or more cannot be reached with a cost above 0.") }    if target > 99.9999m { return err("That target is too close to 100% to give a price.") }    round(margin_calc_price_for(cost, target), minor, "ceiling")}// Reads the texts and answers for them: { answer, bad: { field: message } }.fn margin_calc_work(texts, pair, minor: int) {    let read = reduce(margin_calc_fields, {}, (acc, f) => set(acc, f.id, margin_calc_read(texts[f.id])))    let bad = reduce(pair, {}, (acc, f) => is_err(read[f]) ? set(acc, f, read[f].error) : acc)    if len(bad) > 0 || read[pair[0]] == null || read[pair[1]] == null { return { answer: null, bad } }    { answer: margin_calc_solve(pair[0], pair[1], read, minor), bad }}// The texts with the two fields not set written from the answer, blank without one.fn margin_calc_fill(texts, pair, minor: int) {    let w = margin_calc_work(texts, pair, minor)    let a = w.answer != null && w.answer.error == null ? w.answer : null    let text = (x, digits: int) => x == null ? "" : str(round(x, digits, "half_up"))    reduce(margin_calc_fields, texts, (acc, f) => {        if contains(pair, f.id) { return acc }        set(acc, f.id, a == null ? "" : text(a[f.id], f.ratio ? 2 : minor))    })}// Which two start set, the later first, as it would be had they been typed: the// first two given, an amount always among them.fn margin_calc_first(texts) {    let given = margin_calc_fields | filter((f) => trim(texts[f.id]) != "") | map((f) => f.id)    let amounts = given | filter((f) => f == "cost" || f == "price")    if len(given) < 2 { return ["price", "cost"] }    if len(amounts) == 0 { return [given[0], "cost"] }    [first(given | filter((f) => f != amounts[0])), amounts[0]]}component MarginCalc(cost = null, price = null, margin = null, markup = null, target = null, currency: str = "USD", locale: str? = null,                     title: str = "Margin and markup", id: str = "margin-calc", name: str? = null, on_change = null, class: str? = null) {    style global margin_calc_css    let code = margin_calc_code(currency)    let minor = money.minor_units(code)    let start = { cost: str(cost ?? ""), price: str(price ?? ""), margin: str(margin ?? ""), markup: str(markup ?? "") }    state pair = margin_calc_first(start)    state texts = margin_calc_fill(start, margin_calc_first(start), minor)    state goal = str(target ?? "")    let named = (n: str) => name != null ? `${name}_${n}` : n    let shown = (x) => {        let m = money.of(x, code, "half_up")        is_err(m) ? "a very large amount" : locale != null ? money.format(m, locale) : money.format(m)    }    let work = margin_calc_work(texts, pair, minor)    let a = work.answer != null && work.answer.error == null ? work.answer : null    let fault = work.answer != null ? work.answer.error : null    let want = margin_calc_read(goal)    let goal_bad = is_err(want)    let spent = a != null ? a.cost : margin_calc_read(texts.cost)    let aim = !goal_bad && want != null && spent != null && !is_err(spent) && spent >= 0 ? margin_calc_target(spent, want, minor) : null    let tell = (t, p) => {        let w = margin_calc_work(t, p, minor)        if on_change != null && w.answer != null && w.answer.error == null {            let g = w.answer            on_change({ cost: g.cost, price: g.price, margin: g.margin, markup: g.markup, profit: g.profit })        }    }    let put = (p, field: str, text: str) => {        let t = margin_calc_fill(set(texts, field, text), p, minor)        pair = p        texts = t        tell(t, p)    }    let change = (field: str, text: str) => { put(margin_calc_pair(field, pair[0], pair[1]), field, text) }    // What the rounding did to a margin or markup that was set.    let drift = () => {        if a == null { return null }        let off = (f: str, x) => contains(pair, f) && x != null && round(x, 2, "half_up") != round(dec(margin_calc_read(texts[f])), 2, "half_up")        if off("margin", a.margin) { return `Rounded to the cent, this price gives a margin of ${str(round(a.margin, 2, "half_up"))}%.` }        if off("markup", a.markup) { return `Rounded to the cent, this price gives a markup of ${str(round(a.markup, 2, "half_up"))}%.` }        null    }    <section class={ui_cls(["ui-margincalc", class])} aria-labelledby={`${id}-title`}>        <h3.ui-margincalc-title id={`${id}-title`}>{title}</h3>        <div.ui-margincalc-grid>            {for f in margin_calc_fields => <div key={f.id} class={ui_cls(["ui-margincalc-field", contains(pair, f.id) && "ui-margincalc-set", has(work.bad, f.id) && "ui-margincalc-bad"])}>                <label for={`${id}-${f.id}`}>{f.label}<span.ui-margincalc-tag>{contains(pair, f.id) ? "set" : "worked out"}</span></label>                <div.ui-margincalc-box>                    <input type="text" inputmode="decimal" id={`${id}-${f.id}`} name={named(f.id)} value={texts[f.id]} placeholder="0" autocomplete="off"                           aria-invalid={has(work.bad, f.id) ? "true" : null} @input={(e) => { change(f.id, e.value) }}/>                    <span aria-hidden="true">{f.ratio ? "%" : code}</span>                </div>            </div>}        </div>        <div aria-live="polite">            {len(work.bad) > 0 && <p.ui-margincalc-error role="alert">{work.bad | keys | map((f) => `${f == "cost" ? "Cost" : f == "price" ? "Price" : f == "margin" ? "Margin" : "Markup"} ${work.bad[f]}.`) | join(" ")}</p>}            {fault != null && <p.ui-margincalc-error role="alert">{fault}</p>}            {len(work.bad) == 0 && work.answer == null && <p.ui-margincalc-note>Set any two of the four, one of them an amount.</p>}            {a != null && a.profit < 0 && <p.ui-margincalc-warn>Under cost: each sale loses {shown(0m - a.profit)}.</p>}            {a != null && a.profit == 0 && <p.ui-margincalc-warn>At cost: each sale makes nothing.</p>}            {a != null && drift() != null && <p.ui-margincalc-note>{drift()}</p>}        </div>        {a != null && <dl.ui-margincalc-out>            <div.ui-margincalc-profit><dt>Profit on each</dt><dd>{shown(a.profit)}</dd></div>            <div.ui-margincalc-margin><dt>Margin on the price</dt><dd>{a.margin == null ? "No price" : `${str(round(a.margin, 2, "half_up"))}%`}</dd></div>            <div.ui-margincalc-markup><dt>Markup on the cost</dt><dd>{a.markup == null ? "No cost" : `${str(round(a.markup, 2, "half_up"))}%`}</dd></div>        </dl>}        <div.ui-margincalc-target>            <div class={ui_cls(["ui-margincalc-field", goal_bad && "ui-margincalc-bad"])}>                <label for={`${id}-target`}>Target margin</label>                <div.ui-margincalc-box>                    <input type="text" inputmode="decimal" id={`${id}-target`} name={named("target_margin")} value={goal} placeholder="0" autocomplete="off"                           aria-invalid={goal_bad ? "true" : null} @input={(e) => { goal = e.value }}/>                    <span aria-hidden="true">%</span>                </div>            </div>            <button.ui-margincalc-use type="button" disabled={aim == null || is_err(aim)} @click={() => { put(["price", "cost"], "price", str(aim)) }}>Use this price</button>            <p.ui-margincalc-say aria-live="polite">                {goal_bad ? "Target margin is not a number." : want == null ? "Give a target margin to see the price that reaches it."                    : spent == null || is_err(spent) ? "Set a cost to see the price for this target." : spent < 0 ? "A cost cannot be below 0."                    : is_err(aim) ? aim.error : `Price for ${str(want)}%: ${shown(aim)}.`}            </p>        </div>    </section>}
Thread
36Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.