PluginProbe
Blockenberg — 618 Advanced Gutenberg Blocks, 683 Templates and AI Agent for WordPress Block Editor / 2.0.12
Blockenberg — 618 Advanced Gutenberg Blocks, 683 Templates and AI Agent for WordPress Block Editor v2.0.12
2.0.15 2.0.14 2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / break-even-calculator / frontend.js

frontend.js in Blockenberg — 618 Advanced Gutenberg Blocks, 683 Templates and AI Agent for WordPress Block Editor 2.0.12, at blocks/break-even-calculator/frontend.js

226 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function () {
2 'use strict';
3
4 function calcBEP(fixedCosts, pricePerUnit, varCostUnit, unitsToSell) {
5 var contribution = pricePerUnit - varCostUnit;
6 var bepUnits = contribution > 0 ? fixedCosts / contribution : Infinity;
7 var bepRevenue = isFinite(bepUnits) ? bepUnits * pricePerUnit : Infinity;
8 var profitAtUnits = unitsToSell * contribution - fixedCosts;
9 var marginRatio = pricePerUnit > 0 ? (contribution / pricePerUnit) * 100 : 0;
10 return {
11 bepUnits: Math.ceil(bepUnits),
12 bepRevenue: bepRevenue,
13 contribution: contribution,
14 profitAtUnits: profitAtUnits,
15 marginRatio: marginRatio,
16 revenue: unitsToSell * pricePerUnit,
17 totalCosts: fixedCosts + unitsToSell * varCostUnit
18 };
19 }
20
21 function fmtCur(n, cur) { return cur + Math.abs(n).toLocaleString('en-US', {minimumFractionDigits:0,maximumFractionDigits:0}); }
22
23 function typoCssVarsForEl(typo, prefix, el) {
24 if (!typo) return;
25 if (typo.family) el.style.setProperty(prefix + 'font-family', "'" + typo.family + "', sans-serif");
26 if (typo.weight) el.style.setProperty(prefix + 'font-weight', typo.weight);
27 if (typo.transform) el.style.setProperty(prefix + 'text-transform', typo.transform);
28 if (typo.style) el.style.setProperty(prefix + 'font-style', typo.style);
29 if (typo.decoration) el.style.setProperty(prefix + 'text-decoration', typo.decoration);
30 var su = typo.sizeUnit || 'px';
31 if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) el.style.setProperty(prefix + 'font-size-d', typo.sizeDesktop + su);
32 if (typo.sizeTablet !== '' && typo.sizeTablet != null) el.style.setProperty(prefix + 'font-size-t', typo.sizeTablet + su);
33 if (typo.sizeMobile !== '' && typo.sizeMobile != null) el.style.setProperty(prefix + 'font-size-m', typo.sizeMobile + su);
34 var lhu = typo.lineHeightUnit || '';
35 if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) el.style.setProperty(prefix + 'line-height-d', typo.lineHeightDesktop + lhu);
36 if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) el.style.setProperty(prefix + 'line-height-t', typo.lineHeightTablet + lhu);
37 if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) el.style.setProperty(prefix + 'line-height-m', typo.lineHeightMobile + lhu);
38 var lsu = typo.letterSpacingUnit || 'px';
39 if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) el.style.setProperty(prefix + 'letter-spacing-d', typo.letterSpacingDesktop + lsu);
40 if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) el.style.setProperty(prefix + 'letter-spacing-t', typo.letterSpacingTablet + lsu);
41 if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) el.style.setProperty(prefix + 'letter-spacing-m', typo.letterSpacingMobile + lsu);
42 var wsu = typo.wordSpacingUnit || 'px';
43 if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) el.style.setProperty(prefix + 'word-spacing-d', typo.wordSpacingDesktop + wsu);
44 if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) el.style.setProperty(prefix + 'word-spacing-t', typo.wordSpacingTablet + wsu);
45 if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) el.style.setProperty(prefix + 'word-spacing-m', typo.wordSpacingMobile + wsu);
46 }
47
48 function initBEP(app) {
49 var opts;
50 try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch(e) { opts = {}; }
51
52 var accent = opts.accentColor || '#6c3fb5';
53 var cardBg = opts.cardBg || '#ffffff';
54 var resultBg = opts.resultBg || accent;
55 var resultColor = opts.resultColor || '#ffffff';
56 var profitColor = opts.profitColor || '#10b981';
57 var lossColor = opts.lossColor || '#ef4444';
58 var barBg = opts.barBg || '#e5e7eb';
59 var statBg = opts.statBg || '#f3f4f6';
60 var statBorder = opts.statBorder || '#e5e7eb';
61 var inputBorder = opts.inputBorder || '#e5e7eb';
62 var labelColor = opts.labelColor || '#374151';
63 var cur = opts.currency || '$';
64
65 var cardRadius = (opts.cardRadius || 16) + 'px';
66 var inputRadius = (opts.inputRadius || 8) + 'px';
67 var maxWidth = (opts.maxWidth || 580) + 'px';
68 var resultSize = (opts.resultSize || 46) + 'px';
69 var titleSize = (opts.titleSize || 28) + 'px';
70 var paddingTop = (opts.paddingTop != null ? opts.paddingTop : 60) + 'px';
71 var paddingBottom = (opts.paddingBottom != null ? opts.paddingBottom : 60) + 'px';
72
73 var showProfitZone = opts.showProfitZone !== false;
74 var showContribution = opts.showContribution !== false;
75 var showMarginRatio = opts.showMarginRatio !== false;
76
77 app.style.setProperty('--bkbg-bep-accent', accent);
78 typoCssVarsForEl(opts.typoTitle, '--bkbg-bep-title-', app);
79 typoCssVarsForEl(opts.typoSubtitle, '--bkbg-bep-sub-', app);
80 app.style.paddingTop = paddingTop;
81 app.style.paddingBottom = paddingBottom;
82 if (opts.sectionBg) app.style.background = opts.sectionBg;
83
84 var wrap = document.createElement('div'); wrap.className = 'bkbg-bep-wrap';
85 wrap.style.cssText = 'background:' + cardBg + ';border-radius:' + cardRadius + ';max-width:' + maxWidth;
86 app.appendChild(wrap);
87
88 // Header
89 if ((opts.showTitle && opts.title) || (opts.showSubtitle && opts.subtitle)) {
90 var hdr = document.createElement('div'); hdr.className = 'bkbg-bep-header';
91 if (opts.showTitle && opts.title) {
92 var t = document.createElement('div'); t.className = 'bkbg-bep-title';
93 if (opts.titleColor) t.style.color = opts.titleColor;
94 t.textContent = opts.title; hdr.appendChild(t);
95 }
96 if (opts.showSubtitle && opts.subtitle) {
97 var s = document.createElement('div'); s.className = 'bkbg-bep-subtitle';
98 if (opts.subtitleColor) s.style.color = opts.subtitleColor;
99 s.textContent = opts.subtitle; hdr.appendChild(s);
100 }
101 wrap.appendChild(hdr);
102 }
103
104 function makeLabel(text) {
105 var l = document.createElement('label');
106 l.className = 'bkbg-bep-label'; l.style.color = labelColor; l.textContent = text; return l;
107 }
108 function makeInputField(labelText, defaultVal, hasCur) {
109 var field = document.createElement('div');
110 field.appendChild(makeLabel(labelText));
111 var wrap2 = document.createElement('div'); wrap2.className = 'bkbg-bep-input-wrap';
112 if (hasCur) {
113 var pfx = document.createElement('span'); pfx.className = 'bkbg-bep-prefix'; pfx.textContent = cur;
114 wrap2.appendChild(pfx);
115 }
116 var inp = document.createElement('input');
117 inp.type='number'; inp.value=defaultVal; inp.min='0';
118 inp.className = 'bkbg-bep-input' + (hasCur ? ' has-prefix' : '');
119 inp.style.cssText = 'border:1.5px solid ' + inputBorder + ';border-radius:' + inputRadius;
120 wrap2.appendChild(inp); field.appendChild(wrap2);
121 return { field:field, inp:inp };
122 }
123
124 var grid1 = document.createElement('div'); grid1.className = 'bkbg-bep-grid';
125 var fixedF = makeInputField('Fixed Costs', opts.defaultFixedCosts || 10000, true);
126 var unitsF = makeInputField('Units to Sell', opts.defaultUnitsToSell || 500, false);
127 grid1.appendChild(fixedF.field); grid1.appendChild(unitsF.field); wrap.appendChild(grid1);
128
129 var grid2 = document.createElement('div'); grid2.className = 'bkbg-bep-grid'; grid2.style.marginBottom = '20px';
130 var priceF = makeInputField('Price per Unit', opts.defaultPricePerUnit || 50, true);
131 var varF = makeInputField('Variable Cost / Unit', opts.defaultVarCostUnit || 20, true);
132 grid2.appendChild(priceF.field); grid2.appendChild(varF.field); wrap.appendChild(grid2);
133
134 // Result card
135 var resultCard = document.createElement('div'); resultCard.className = 'bkbg-bep-result-card';
136 resultCard.style.cssText = 'background:' + resultBg + ';color:' + resultColor;
137 var rLbl = document.createElement('div'); rLbl.className = 'bkbg-bep-result-label'; rLbl.textContent = 'Break-Even Point';
138 var rVal = document.createElement('div'); rVal.className = 'bkbg-bep-result-value'; rVal.style.fontSize = resultSize;
139 var rSub = document.createElement('div'); rSub.className = 'bkbg-bep-result-sub';
140 resultCard.appendChild(rLbl); resultCard.appendChild(rVal); resultCard.appendChild(rSub);
141 wrap.appendChild(resultCard);
142
143 // Progress bar
144 var progressSection, fillEl, midlineEl, progressNote, progressRight;
145 if (showProfitZone) {
146 progressSection = document.createElement('div'); progressSection.className = 'bkbg-bep-progress-section';
147 var progressHdr = document.createElement('div'); progressHdr.className = 'bkbg-bep-progress-header';
148 var progressLeft = document.createElement('span'); progressLeft.textContent = '0 units';
149 progressRight = document.createElement('span');
150 progressHdr.appendChild(progressLeft); progressHdr.appendChild(progressRight);
151 var track = document.createElement('div'); track.className = 'bkbg-bep-track';
152 track.style.background = barBg;
153 fillEl = document.createElement('div'); fillEl.className = 'bkbg-bep-fill';
154 midlineEl = document.createElement('div'); midlineEl.className = 'bkbg-bep-midline';
155 midlineEl.style.left = '50%';
156 track.appendChild(fillEl); track.appendChild(midlineEl);
157 progressNote = document.createElement('div'); progressNote.className = 'bkbg-bep-progress-note';
158 progressSection.appendChild(progressHdr);
159 progressSection.appendChild(track);
160 progressSection.appendChild(progressNote);
161 wrap.appendChild(progressSection);
162 }
163
164 // Stats row
165 var statsRow = document.createElement('div'); statsRow.className = 'bkbg-bep-stats';
166 function makeStat(lbl, color) {
167 var card = document.createElement('div'); card.className = 'bkbg-bep-stat';
168 card.style.cssText = 'background:' + statBg + ';border:1px solid ' + statBorder;
169 var v = document.createElement('div'); v.className = 'bkbg-bep-stat-val'; if(color) v.style.color=color;
170 var l = document.createElement('div'); l.className = 'bkbg-bep-stat-lbl'; l.textContent=lbl;
171 card.appendChild(v); card.appendChild(l); return {card:card,val:v};
172 }
173 var profitStat = makeStat('Profit / Loss');
174 var revStat = makeStat('Total Revenue');
175 var costStat = makeStat('Total Costs');
176 statsRow.appendChild(profitStat.card); statsRow.appendChild(revStat.card); statsRow.appendChild(costStat.card);
177 wrap.appendChild(statsRow);
178
179 var extrasRow;
180 if (showContribution || showMarginRatio) {
181 extrasRow = document.createElement('div'); extrasRow.className = 'bkbg-bep-extras';
182 if (showContribution) { var cm = makeStat('Contribution Margin / Unit'); cm.val.style.color=accent; extrasRow.appendChild(cm.card); wrap._cmStat=cm; }
183 if (showMarginRatio) { var mr = makeStat('Contribution Margin Ratio'); mr.val.style.color=accent; extrasRow.appendChild(mr.card); wrap._mrStat=mr; }
184 wrap.appendChild(extrasRow);
185 }
186
187 function refresh() {
188 var fc = parseFloat(fixedF.inp.value) || 0;
189 var pu = parseFloat(priceF.inp.value) || 0;
190 var vc = parseFloat(varF.inp.value) || 0;
191 var u = parseFloat(unitsF.inp.value) || 0;
192 var r = calcBEP(fc, pu, vc, u);
193 var pos = r.profitAtUnits >= 0;
194
195 rVal.textContent = isFinite(r.bepUnits) ? r.bepUnits.toLocaleString() + ' units' : '∞';
196 rSub.textContent = isFinite(r.bepRevenue)
197 ? 'Revenue needed: ' + fmtCur(r.bepRevenue, cur)
198 : 'Price must exceed variable cost';
199
200 profitStat.val.textContent = (r.profitAtUnits<0?'-':'') + fmtCur(r.profitAtUnits, cur);
201 profitStat.val.style.color = pos ? profitColor : lossColor;
202 revStat.val.textContent = fmtCur(r.revenue, cur); revStat.val.style.color = '#111827';
203 costStat.val.textContent = fmtCur(r.totalCosts, cur); costStat.val.style.color = '#111827';
204
205 if (showContribution && wrap._cmStat) wrap._cmStat.val.textContent = fmtCur(r.contribution, cur);
206 if (showMarginRatio && wrap._mrStat) wrap._mrStat.val.textContent = r.marginRatio.toFixed(1) + '%';
207
208 if (showProfitZone && isFinite(r.bepUnits)) {
209 var progress = r.bepUnits > 0 ? Math.min(100, (u / r.bepUnits) * 100) : 100;
210 fillEl.style.width = progress + '%';
211 fillEl.style.background = pos ? profitColor : lossColor;
212 progressRight.textContent = u.toLocaleString() + ' units → ' + (pos ? '✓ PROFIT' : '✗ LOSS');
213 progressRight.style.color = pos ? profitColor : lossColor;
214 progressNote.textContent = '↑ Break-even at ' + r.bepUnits.toLocaleString() + ' units';
215 }
216 }
217
218 [fixedF.inp, priceF.inp, varF.inp, unitsF.inp].forEach(function(inp) {
219 inp.addEventListener('input', refresh);
220 });
221 refresh();
222 }
223
224 document.querySelectorAll('.bkbg-bep-app').forEach(initBEP);
225 })();
226