diff --git a/web/src/harvest.ts b/web/src/harvest.ts index 80ef6aaf..f197301e 100644 --- a/web/src/harvest.ts +++ b/web/src/harvest.ts @@ -40,29 +40,49 @@ function renderHarvestables(capital_gains: CapitalGain[]) { .append("div") .attr("class", "card-header-icon") .style("flex-grow", "1") + .style("cursor", "auto") .append("div") .each(function (cg) { const self = d3.select(this); - const [units, amount, taxableGain] = unitsRequired(cg, 100000); - self.append("span").html("Redeeming "); + const [units, amount, taxableGain] = unitsRequiredFromGain(cg, 100000); + self.append("span").html("If you redeem "); const unitsSpan = self.append("span").text(formatFloat(units)); - self.append("span").html(" units (amount ₹"); - const amountSpan = self.append("span").text(formatCurrency(amount)); - self.append("span").html(") will result in taxable gain ₹"); + self.append("span").html(" units you will get ₹"); + const amountInput = self + .append("input") + .attr("class", "input is-small adjustable-input") + .attr("type", "number") + .attr("value", round(amount)) + .attr("step", "1000") + .on("input", (event) => { + const [units, amount, taxableGain] = unitsRequiredFromAmount( + cg, + parseInt(event.srcElement.value) + ); + + unitsSpan.text(formatFloat(units)); + (taxableGainInput.node() as HTMLInputElement).value = + round(taxableGain).toString(); + event.srcElement.value = round(amount); + }); self + .append("span") + .html("  and your taxable gain would be ₹"); + const taxableGainInput = self .append("input") .attr("class", "input is-small adjustable-input") .attr("type", "number") .attr("value", round(taxableGain)) .attr("step", "1000") .on("input", (event) => { - const [units, amount, taxableGain] = unitsRequired( + const [units, amount, taxableGain] = unitsRequiredFromGain( cg, - event.srcElement.value + parseInt(event.srcElement.value) ); unitsSpan.text(formatFloat(units)); event.srcElement.value = round(taxableGain); - amountSpan.text(formatCurrency(amount)); + (amountInput.node() as HTMLInputElement).value = + round(amount).toString(); }); }); @@ -137,7 +157,7 @@ function renderHarvestables(capital_gains: CapitalGain[]) { .append("div") .attr("class", "table-container") .style("overflow-y", "auto") - .style("max-height", "210px") + .style("max-height", "245px") .append("table") .attr("class", "table"); @@ -182,7 +202,7 @@ function renderHarvestables(capital_gains: CapitalGain[]) { }); } -function unitsRequired( +function unitsRequiredFromGain( cg: CapitalGain, taxableGain: number ): [number, number, number] { @@ -208,6 +228,30 @@ function unitsRequired( return [units, amount, gain]; } +function unitsRequiredFromAmount( + cg: CapitalGain, + expectedAmount: number +): [number, number, number] { + let gain = 0; + let amount = 0; + let units = 0; + const available = _.clone(cg.harvestable.harvest_breakdown); + while (expectedAmount > amount && available.length > 0) { + const breakdown = available.shift(); + if (breakdown.current_price < expectedAmount - amount) { + gain += breakdown.taxable_unrealized_gain; + units += breakdown.units; + amount += breakdown.current_price; + } else { + const u = (expectedAmount - amount) / cg.harvestable.current_unit_price; + units += u; + amount = expectedAmount; + gain += u * (breakdown.taxable_unrealized_gain / breakdown.units); + } + } + return [units, amount, gain]; +} + function renderSingleBar(cg: CapitalGain) { const selection = d3.select(this); const svg = selection.append("svg"); diff --git a/web/static/styles/custom.css b/web/static/styles/custom.css index b9dd4de0..a6a733a1 100644 --- a/web/static/styles/custom.css +++ b/web/static/styles/custom.css @@ -96,13 +96,16 @@ body { } .adjustable-input { + border: none; + border-bottom: 1px solid #ccc; + box-shadow: none; width: 80px; - font-weight: bold; - font-size: 0.75rem !important; + font-size: 1em !important; margin-left: 5px; padding-top: 0; padding-bottom: 0; padding-right: 0; + padding-left: 5px; height: 1.3rem; }