banking_views.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. const { div, h2, p, section, button, form, a, input, span, pre, table, thead, tbody, tr, td, th, br } = require("../server/node_modules/hyperaxe");
  2. const { template, i18n, userLink } = require("../views/main_views");
  3. const moment = require("../server/node_modules/moment");
  4. const FILTER_LABELS = {
  5. overview: i18n.bankOverview,
  6. exchange: i18n.bankExchange,
  7. mine: i18n.mine,
  8. pending: i18n.pending,
  9. closed: i18n.closed,
  10. claimed: i18n.bankStatusClaimed,
  11. expired: i18n.bankStatusExpired,
  12. epochs: i18n.bankEpochs,
  13. rules: i18n.bankRules,
  14. addresses: i18n.bankAddresses
  15. };
  16. const generateFilterButtons = (filters, currentFilter, action) =>
  17. div({ class: "mode-buttons-row" },
  18. ...filters.map(mode =>
  19. form({ method: "GET", action },
  20. input({ type: "hidden", name: "filter", value: mode }),
  21. button({ type: "submit", class: currentFilter === mode ? "filter-btn active" : "filter-btn" }, (FILTER_LABELS[mode] || mode).toUpperCase())
  22. )
  23. )
  24. );
  25. const kvRow = (label, value) =>
  26. tr(td({ class: "card-label" }, label), td({ class: "card-value" }, value));
  27. const fmtIndex = (value) => {
  28. return value ? value.toFixed(6) : "0.000000";
  29. };
  30. const pct = (value) => {
  31. if (value === undefined || value === null) return "0.000001%";
  32. const formattedValue = (value).toFixed(6);
  33. const sign = value >= 0 ? "+" : "";
  34. return `${sign}${formattedValue}%`;
  35. };
  36. const fmtDate = (timestamp) => {
  37. return moment(timestamp).format('YYYY-MM-DD HH:mm:ss');
  38. };
  39. const fmtEcoTime = (ms) => {
  40. if (!ms || ms <= 0) return `0 ${i18n.bankUnitMs || 'ms'}`;
  41. if (ms < 1000) return `${Number(ms).toFixed(3)} ${i18n.bankUnitMs || 'ms'}`;
  42. const s = ms / 1000;
  43. if (s < 60) return `${s.toFixed(2)} ${i18n.bankUnitSeconds || 'seconds'}`;
  44. const m = s / 60;
  45. if (m < 60) return `${m.toFixed(2)} ${i18n.bankUnitMinutes || 'minutes'}`;
  46. const h = m / 60;
  47. if (h < 24) return `${h.toFixed(2)} ${i18n.bankHoursOfWork || 'hours'}`;
  48. return `${(h / 24).toFixed(2)} ${i18n.bankUnitDays || 'days'}`;
  49. };
  50. const escAttr = (s) => String(s)
  51. .replace(/&/g, '&amp;')
  52. .replace(/</g, '&lt;')
  53. .replace(/>/g, '&gt;')
  54. .replace(/"/g, '&quot;')
  55. .replace(/'/g, '&#39;');
  56. const buildEcoValueChartSvg = (history, labels) => {
  57. const arr = Array.isArray(history) ? history.slice(-120) : [];
  58. const W = 720, H = 320;
  59. const padL = 56, padR = 16, padT = 16, padB = 70;
  60. const plotW = W - padL - padR;
  61. const plotH = H - padT - padB;
  62. if (arr.length < 2) {
  63. return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" class="bank-eco-chart-svg" preserveAspectRatio="xMidYMid meet">`
  64. + `<rect x="0" y="0" width="${W}" height="${H}" class="bank-eco-chart-bg" />`
  65. + `<text x="${W/2}" y="${H/2}" text-anchor="middle" class="bank-eco-chart-empty">${escAttr(labels.empty || 'Not enough samples yet')}</text>`
  66. + `</svg>`;
  67. }
  68. const values = arr.map(s => Number(s.ecoValue || 0));
  69. const supplies = arr.map(s => Number(s.currentSupply || 0));
  70. const inflations = arr.map(s => Number(s.inflationFactor || 0));
  71. const minV = Math.min(...values);
  72. const maxV = Math.max(...values);
  73. const rangeV = maxV - minV || 1;
  74. const minS = Math.min(...supplies);
  75. const maxS = Math.max(...supplies);
  76. const rangeS = maxS - minS || 1;
  77. const minI = Math.min(...inflations);
  78. const maxI = Math.max(...inflations);
  79. const rangeI = maxI - minI || 1;
  80. const stepX = arr.length > 1 ? plotW / (arr.length - 1) : plotW;
  81. const xy = (i, v, minR, rangeR) => {
  82. const x = padL + i * stepX;
  83. const y = padT + plotH - ((v - minR) / rangeR) * plotH;
  84. return `${x.toFixed(2)},${y.toFixed(2)}`;
  85. };
  86. const pointsValue = values.map((v, i) => xy(i, v, minV, rangeV)).join(' ');
  87. const pointsSupply = supplies.map((v, i) => xy(i, v, minS, rangeS)).join(' ');
  88. const pointsInfl = inflations.map((v, i) => xy(i, v, minI, rangeI)).join(' ');
  89. const tsStart = moment(arr[0].ts).format('YYYY-MM-DD HH:mm');
  90. const tsEnd = moment(arr[arr.length - 1].ts).format('YYYY-MM-DD HH:mm');
  91. const tsMid = moment(arr[Math.floor(arr.length / 2)].ts).format('YYYY-MM-DD HH:mm');
  92. const yTicks = 4;
  93. const grid = [];
  94. for (let i = 0; i <= yTicks; i++) {
  95. const y = padT + (plotH / yTicks) * i;
  96. grid.push(`<line x1="${padL}" x2="${W - padR}" y1="${y.toFixed(2)}" y2="${y.toFixed(2)}" class="bank-eco-chart-grid" />`);
  97. const val = maxV - (rangeV / yTicks) * i;
  98. grid.push(`<text x="${padL - 6}" y="${(y + 4).toFixed(2)}" text-anchor="end" class="bank-eco-chart-axis">${val.toFixed(4)}</text>`);
  99. }
  100. const xLabelY = padT + plotH + 16;
  101. const xLabels = `<text x="${padL}" y="${xLabelY}" text-anchor="start" class="bank-eco-chart-axis">${escAttr(tsStart)}</text>`
  102. + `<text x="${(padL + plotW/2).toFixed(2)}" y="${xLabelY}" text-anchor="middle" class="bank-eco-chart-axis">${escAttr(tsMid)}</text>`
  103. + `<text x="${W - padR}" y="${xLabelY}" text-anchor="end" class="bank-eco-chart-axis">${escAttr(tsEnd)}</text>`;
  104. const legendY = padT + plotH + 44;
  105. const legendBaseX = padL;
  106. const legend = `<g class="bank-eco-chart-legend">`
  107. + `<rect x="${legendBaseX}" y="${(legendY - 7).toFixed(2)}" width="14" height="3" class="bank-eco-chart-line-value-legend" />`
  108. + `<text x="${(legendBaseX + 18).toFixed(2)}" y="${legendY}" class="bank-eco-chart-legend-text">${escAttr(labels.value || 'Value')}</text>`
  109. + `<rect x="${(legendBaseX + 170).toFixed(2)}" y="${(legendY - 7).toFixed(2)}" width="14" height="3" class="bank-eco-chart-line-supply-legend" />`
  110. + `<text x="${(legendBaseX + 188).toFixed(2)}" y="${legendY}" class="bank-eco-chart-legend-text">${escAttr(labels.supply || 'Supply')}</text>`
  111. + `<rect x="${(legendBaseX + 320).toFixed(2)}" y="${(legendY - 7).toFixed(2)}" width="14" height="3" class="bank-eco-chart-line-inflation-legend" />`
  112. + `<text x="${(legendBaseX + 338).toFixed(2)}" y="${legendY}" class="bank-eco-chart-legend-text">${escAttr(labels.inflation || 'Inflation')}</text>`
  113. + `</g>`;
  114. return `<svg viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" class="bank-eco-chart-svg" preserveAspectRatio="xMidYMid meet">`
  115. + `<rect x="0" y="0" width="${W}" height="${H}" class="bank-eco-chart-bg" />`
  116. + grid.join('')
  117. + `<polyline points="${pointsSupply}" class="bank-eco-chart-line-supply" />`
  118. + `<polyline points="${pointsInfl}" class="bank-eco-chart-line-inflation" />`
  119. + `<polyline points="${pointsValue}" class="bank-eco-chart-line-value" />`
  120. + xLabels
  121. + legend
  122. + `</svg>`;
  123. };
  124. const renderExchange = (ex, history) => {
  125. if (!ex) return div(p(i18n.bankExchangeNoData));
  126. const syncStatus = ex.isSynced ? i18n.bankingSyncStatusSynced : i18n.bankingSyncStatusOutdated;
  127. const syncStatusClass = ex.isSynced ? 'synced' : 'outdated';
  128. const ecoTimeLabel = ex.isSynced ? fmtEcoTime(ex.ecoTimeMs) : fmtEcoTime(0);
  129. const chartLabels = {
  130. value: i18n.bankExchangeChartValue || 'Value (ECO/h)',
  131. supply: i18n.bankExchangeChartSupply || 'Supply',
  132. inflation: i18n.bankExchangeChartInflation || 'Inflation %',
  133. empty: i18n.bankExchangeChartEmpty || 'Not enough samples yet — revisit later'
  134. };
  135. const hasEnoughSamples = Array.isArray(history) && history.length >= 2 && ex.isSynced;
  136. return div(
  137. div({ class: "bank-summary" },
  138. table({ class: "bank-info-table" },
  139. tbody(
  140. kvRow(i18n.bankingSyncStatus,
  141. span({ class: syncStatusClass }, syncStatus)
  142. ),
  143. kvRow(i18n.bankExchangeCurrentValue, `${fmtIndex(ex.ecoValue)} ECO`),
  144. kvRow(i18n.bankCurrentSupply, `${Number(ex.currentSupply || 0).toFixed(6)} ECO`),
  145. kvRow(i18n.bankTotalSupply, `${Number(ex.totalSupply || 0).toFixed(6)} ECO`),
  146. kvRow(i18n.bankEcoinHours, ecoTimeLabel),
  147. kvRow(i18n.bankInflation, `${ex.inflationFactor.toFixed(2)}%`),
  148. kvRow(i18n.bankInflationMonthly, `${Number(ex.inflationMonthly || 0).toFixed(2)}%`)
  149. )
  150. )
  151. ),
  152. hasEnoughSamples
  153. ? div({ class: "bank-eco-chart-block" },
  154. h2({ class: "bank-eco-chart-title" }, i18n.bankExchangeChartTitle || 'ECOin value over time'),
  155. div({ class: "bank-eco-chart-canvas", innerHTML: buildEcoValueChartSvg(history, chartLabels) })
  156. )
  157. : null
  158. );
  159. };
  160. const renderOverviewSummaryTable = (s, rules) => {
  161. const score = Number(s.userEngagementScore || 0);
  162. const pool = Number(s.pool || 0);
  163. const W = Math.max(1, Number(s.weightsSum || 1));
  164. const w = 1 + score / 100;
  165. const cap = rules?.caps?.cap_user_epoch ?? 50;
  166. const future = Math.min(pool * (w / W), cap);
  167. const availClass = s.ubiAvailability === "OK" ? "ubi-available" : "ubi-unavailable";
  168. const availLabel = s.ubiAvailability === "OK" ? i18n.bankUbiAvailableOk : i18n.bankUbiAvailableNo;
  169. return div({ class: "bank-summary" },
  170. table({ class: "bank-info-table" },
  171. tbody(
  172. kvRow(i18n.bankUserBalance, `${Number(s.userBalance || 0).toFixed(6)} ECO`),
  173. kvRow(i18n.bankUbiAvailability, span({ class: availClass }, availLabel)),
  174. s.pubId ? kvRow(i18n.pubIdLabel, userLink(s.pubId)) : null,
  175. kvRow(i18n.bankEpoch, String(s.epochId || "-")),
  176. kvRow(i18n.bankPool, `${pool.toFixed(6)} ECO`),
  177. kvRow(i18n.bankWeightsSum, String(W.toFixed(6))),
  178. kvRow(i18n.bankingUserEngagementScore, String(score)),
  179. kvRow(i18n.bankUbiThisMonth, `${future.toFixed(6)} ECO`)
  180. )
  181. )
  182. );
  183. };
  184. const renderClaimUBIBlock = (pendingAllocation, isPub, alreadyClaimed, pubId, hasValidWallet, ubiAvailability) => {
  185. if (alreadyClaimed) return "";
  186. if (!pubId && !isPub) return "";
  187. if (!isPub && !hasValidWallet) return "";
  188. if (!isPub && ubiAvailability !== "OK") return "";
  189. if (!pendingAllocation && !isPub) {
  190. return div({ class: "bank-claim-ubi" },
  191. div({ class: "bank-claim-card" },
  192. form({ method: "POST", action: "/banking/claim-ubi" },
  193. button({ type: "submit", class: "create-button bank-claim-btn" }, i18n.bankClaimUBI)
  194. )
  195. )
  196. );
  197. }
  198. if (!pendingAllocation) return "";
  199. return div({ class: "bank-claim-ubi" },
  200. div({ class: "bank-claim-card" },
  201. p(`${i18n.bankUbiThisMonth}: `, span({ class: "accent" }, `${Number(pendingAllocation.amount || 0).toFixed(6)} ECO`)),
  202. p(`${i18n.bankEpoch}: `, span(pendingAllocation.concept || "")),
  203. form({ method: "POST", action: `/banking/claim/${encodeURIComponent(pendingAllocation.id)}` },
  204. button({ type: "submit", class: "create-button bank-claim-btn" }, isPub ? i18n.bankClaimAndPay : i18n.bankClaimUBI)
  205. )
  206. )
  207. );
  208. };
  209. const filterAllocations = (allocs, filter, userId) => {
  210. if (filter === "mine") return allocs.filter(a => a.to === userId && (a.status === "UNCLAIMED" || a.status === "UNCONFIRMED"));
  211. if (filter === "pending") return allocs.filter(a => a.status === "UNCLAIMED" || a.status === "UNCONFIRMED");
  212. if (filter === "closed") return allocs.filter(a => a.status === "CLOSED");
  213. if (filter === "claimed") return allocs.filter(a => a.status === "CLAIMED");
  214. if (filter === "expired") return allocs.filter(a => a.status === "EXPIRED");
  215. return allocs;
  216. };
  217. const allocationsTable = (rows = [], userId) =>
  218. rows.length === 0
  219. ? div(p(i18n.bankNoAllocations))
  220. : table(
  221. { class: "bank-allocs" },
  222. thead(
  223. tr(
  224. th(i18n.bankAllocDate),
  225. th(i18n.bankAllocConcept),
  226. th(i18n.bankAllocFrom),
  227. th(i18n.bankAllocTo),
  228. th(i18n.bankAllocAmount),
  229. th(i18n.bankAllocStatus),
  230. th("")
  231. )
  232. ),
  233. tbody(
  234. ...rows.map(r =>
  235. tr(
  236. td(new Date(r.createdAt).toLocaleString()),
  237. td(r.concept || ""),
  238. td(userLink(r.from)),
  239. td(userLink(r.to)),
  240. td(String(Number(r.amount || 0).toFixed(6))),
  241. td(r.status),
  242. td(
  243. (r.status === "UNCLAIMED" || r.status === "UNCONFIRMED") && r.to === userId
  244. ? form({ method: "POST", action: `/banking/claim/${encodeURIComponent(r.id)}` },
  245. button({ type: "submit", class: "filter-btn" }, i18n.bankClaimNow)
  246. )
  247. : r.status === "CLOSED" && r.txid
  248. ? a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(r.txid)}`, target: "_blank", class: "btn-singleview" }, i18n.bankViewTx)
  249. : null
  250. )
  251. )
  252. )
  253. )
  254. );
  255. const renderEpochList = (epochs = []) =>
  256. epochs.length === 0
  257. ? div(p(i18n.bankNoEpochs))
  258. : table(
  259. { class: "bank-epochs" },
  260. thead(tr(th(i18n.bankEpochId), th(i18n.bankPool), th(i18n.bankWeightsSum), th(i18n.bankRuleHash), th(""))),
  261. tbody(
  262. ...epochs
  263. .sort((a, b) => String(b.id).localeCompare(String(a.id)))
  264. .map(e =>
  265. tr(
  266. td(e.id),
  267. td(String(Number(e.pool || 0).toFixed(6))),
  268. td(String(Number(e.weightsSum || 0).toFixed(6))),
  269. td(e.hash || "-"),
  270. td(
  271. form({ method: "GET", action: `/banking/epoch/${encodeURIComponent(e.id)}` },
  272. button({ type: "submit", class: "filter-btn" }, i18n.bankViewEpoch)
  273. )
  274. )
  275. )
  276. )
  277. )
  278. );
  279. const rulesBlock = (rules) =>
  280. div({ class: "bank-rules" }, pre({ class: "json-content" }, JSON.stringify(rules || {}, null, 2)));
  281. const flashText = (key) => {
  282. if (key === "added") return i18n.bankAddressAdded;
  283. if (key === "updated") return i18n.bankAddressUpdated;
  284. if (key === "exists") return i18n.bankAddressExists;
  285. if (key === "invalid") return i18n.bankAddressInvalid;
  286. if (key === "deleted") return i18n.bankAddressDeleted;
  287. if (key === "not_found") return i18n.bankAddressNotFound;
  288. if (key === "claimed_pending") return i18n.bankClaimedPending;
  289. if (key === "already_claimed") return i18n.bankAlreadyClaimedThisMonth;
  290. if (key === "no_pub_configured") return i18n.bankNoPubConfigured;
  291. if (key === "no_funds") return i18n.bankUbiAvailableNo;
  292. if (key === "forbidden") return i18n.bankAddressForbidden;
  293. return "";
  294. };
  295. const flashBanner = (msgKey) =>
  296. !msgKey ? null : div({ class: "flash-banner" }, p(flashText(msgKey) || msgKey));
  297. const addressesToolbar = (rows = [], search = "") =>
  298. div({ class: "addr-toolbar" },
  299. div({ class: "addr-counter accent-pill" },
  300. span({ class: "acc-title accent" }, i18n.bankAddressTotal + ":"),
  301. span({ class: "acc-badge" }, String(rows.length))
  302. ),
  303. form({ method: "GET", action: "/banking", class: "addr-search" },
  304. input({ type: "hidden", name: "filter", value: "addresses" }),
  305. input({ type: "text", name: "q", placeholder: i18n.bankAddressSearch, value: search || "" }),
  306. br(),
  307. button({ type: "submit", class: "filter-btn" }, i18n.search)
  308. )
  309. );
  310. const renderAddresses = (data, userId) => {
  311. const rows = data.addresses || [];
  312. const search = data.search || "";
  313. return div(
  314. data.flash ? flashBanner(data.flash) : null,
  315. addressesToolbar(rows, search),
  316. div({ class: "bank-addresses-stack" },
  317. div({ class: "addr-form-card wide" },
  318. h2(i18n.bankAddAddressTitle),
  319. form({ method: "POST", action: "/banking/addresses", class: "addr-form" },
  320. div({ class: "form-row" },
  321. span({ class: "form-label accent" }, i18n.bankAddAddressUser + ":"),
  322. input({
  323. class: "form-input xl",
  324. type: "text",
  325. name: "userId",
  326. required: true,
  327. pattern: "^@[A-Za-z0-9+/]+={0,2}\\.ed25519$",
  328. placeholder: "@...=.ed25519",
  329. id: "addr-user-id"
  330. })
  331. ),
  332. div({ class: "form-row" },
  333. span({ class: "form-label accent" }, i18n.bankAddAddressAddress + ":"),
  334. input({
  335. class: "form-input xl",
  336. type: "text",
  337. name: "address",
  338. required: true,
  339. pattern: "^[A-Za-z0-9]{20,64}$",
  340. placeholder: "ETQ17sBv8QFoiCPGKDQzNcDJeXmB2317HX"
  341. })
  342. ),
  343. div({ class: "form-actions" },
  344. button({ type: "submit", class: "filter-btn" }, i18n.bankAddAddressSave)
  345. )
  346. )
  347. ),
  348. div({ class: "addr-list-card" },
  349. rows.length === 0
  350. ? div(p(i18n.bankNoAddresses))
  351. : table(
  352. { class: "bank-addresses" },
  353. thead(
  354. tr(
  355. th(i18n.bankUser),
  356. th(i18n.bankAddress),
  357. th(i18n.bankAddressSource),
  358. th(i18n.bankAddressActions)
  359. )
  360. ),
  361. tbody(
  362. ...rows.map(r =>
  363. tr(
  364. td(userLink(r.id)),
  365. td(r.address),
  366. td(r.source === "local" ? i18n.bankLocal : i18n.bankFromOasis),
  367. td(
  368. div({ class: "row-actions" },
  369. form({ method: "POST", action: "/banking/addresses/delete", class: "addr-del" },
  370. input({ type: "hidden", name: "userId", value: r.id }),
  371. input({ type: "hidden", name: "source", value: r.source || "local" }),
  372. button({ type: "submit", class: "delete-btn", onclick: `return confirm(${JSON.stringify(i18n.bankAddressDeleteConfirm)})` }, i18n.bankAddressDelete)
  373. )
  374. )
  375. )
  376. )
  377. )
  378. )
  379. )
  380. )
  381. )
  382. );
  383. };
  384. const renderBankingView = (data, filter, userId, isPub) =>
  385. template(
  386. i18n.banking,
  387. section(
  388. div({ class: "tags-header" }, h2(i18n.banking), p(i18n.bankingDescription)),
  389. data.flash ? div({ class: "flash-banner" }, p(flashText(data.flash) || data.flash)) : null,
  390. generateFilterButtons(["overview","exchange","mine","pending","closed","claimed","expired","epochs","rules","addresses"], filter, "/banking"),
  391. filter === "overview"
  392. ? div(
  393. renderOverviewSummaryTable(data.summary || {}, data.rules),
  394. renderClaimUBIBlock(data.pendingUBI || null, isPub, data.alreadyClaimed, (data.summary || {}).pubId, (data.summary || {}).hasValidWallet, (data.summary || {}).ubiAvailability),
  395. allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId)
  396. )
  397. : filter === "exchange"
  398. ? renderExchange(data.exchange, data.exchangeHistory)
  399. : filter === "epochs"
  400. ? renderEpochList(data.epochs || [])
  401. : filter === "rules"
  402. ? rulesBlock(data.rules || {})
  403. : filter === "addresses"
  404. ? renderAddresses(data, userId)
  405. : allocationsTable(
  406. filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId),
  407. userId
  408. )
  409. )
  410. );
  411. const renderSingleAllocationView = (alloc, userId) => {
  412. if (!alloc) return template(i18n.banking, section(div(p(i18n.bankNoAllocations))));
  413. return template(
  414. i18n.banking,
  415. section(
  416. div({ class: "tags-header" }, h2(i18n.banking)),
  417. div({ class: "bank-summary" },
  418. table({ class: "bank-info-table" },
  419. tbody(
  420. kvRow("ID", alloc.id || "-"),
  421. kvRow(i18n.bankAllocConcept, alloc.concept || "-"),
  422. kvRow(i18n.bankAllocFrom, alloc.from || "-"),
  423. kvRow(i18n.bankAllocTo, alloc.to || "-"),
  424. kvRow(i18n.bankAllocAmount, `${Number(alloc.amount || 0).toFixed(6)} ECO`),
  425. kvRow(i18n.bankAllocStatus, alloc.status || "-"),
  426. kvRow(i18n.bankAllocDate, alloc.createdAt ? fmtDate(alloc.createdAt) : "-"),
  427. alloc.txid ? kvRow("TxID", a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${encodeURIComponent(alloc.txid)}`, target: "_blank" }, alloc.txid)) : null
  428. )
  429. )
  430. ),
  431. alloc.status === "UNCONFIRMED" && alloc.to === userId
  432. ? form({ method: "POST", action: `/banking/claim/${encodeURIComponent(alloc.id)}` },
  433. button({ type: "submit", class: "filter-btn" }, i18n.bankClaimNow)
  434. )
  435. : null,
  436. div(a({ href: "/banking", class: "filter-btn" }, i18n.bankOverview))
  437. )
  438. );
  439. };
  440. const renderEpochView = (epoch, allocations) => {
  441. if (!epoch) return template(i18n.banking, section(div(p(i18n.bankNoEpochs))));
  442. return template(
  443. i18n.banking,
  444. section(
  445. div({ class: "tags-header" }, h2(`${i18n.bankEpoch}: ${epoch.id}`)),
  446. div({ class: "bank-summary" },
  447. table({ class: "bank-info-table" },
  448. tbody(
  449. kvRow(i18n.bankEpochId, epoch.id || "-"),
  450. kvRow(i18n.bankPool, `${Number(epoch.pool || 0).toFixed(6)} ECO`),
  451. kvRow(i18n.bankWeightsSum, String(Number(epoch.weightsSum || 0).toFixed(6))),
  452. kvRow(i18n.bankRuleHash, epoch.hash || "-")
  453. )
  454. )
  455. ),
  456. h2(i18n.bankEpochAllocations),
  457. allocationsTable(allocations || [], "")
  458. )
  459. );
  460. };
  461. module.exports = { renderBankingView, renderSingleAllocationView, renderEpochView };