{"id":3595,"date":"2025-10-17T00:58:07","date_gmt":"2025-10-16T17:58:07","guid":{"rendered":"https:\/\/hcbiopharm.net\/?page_id=3595"},"modified":"2025-10-17T00:59:54","modified_gmt":"2025-10-16T17:59:54","slug":"gpt51","status":"publish","type":"page","link":"https:\/\/hcbiopharm.net\/en\/gpt51\/","title":{"rendered":"gpt51"},"content":{"rendered":"<div data-elementor-type=\"wp-page\" data-elementor-id=\"3595\" class=\"elementor elementor-3595\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-120b692 e-flex e-con-boxed e-con e-parent\" data-id=\"120b692\" data-element_type=\"container\">\r\n\t\t\t\t\t<div class=\"e-con-inner\">\r\n\t\t\t\t<div class=\"elementor-element elementor-element-f36c461 elementor-widget elementor-widget-html\" data-id=\"f36c461\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t\r\n<!DOCTYPE html>\r\n<html lang=\"ko\">\r\n<head>\r\n<meta charset=\"utf-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\r\n<title>\uac00\uacc4\ubd80 \u00b7 Wealth Ledger (VND)<\/title>\r\n<style>\r\n  :root{\r\n    --bg: #0c0f16;\r\n    --bg-soft: #111624;\r\n    --card: rgba(255,255,255,0.06);\r\n    --card-strong: rgba(255,255,255,0.1);\r\n    --text: #e8ecf4;\r\n    --muted: #9aa5b1;\r\n    --accent: #4e8cff;\r\n    --accent2: #7d60ff;\r\n    --good: #22c55e;\r\n    --bad: #ef4444;\r\n    --warn: #f59e0b;\r\n    --border: rgba(255,255,255,0.14);\r\n    --shadow: 0 10px 30px rgba(0,0,0,0.35);\r\n    --radius: 14px;\r\n  }\r\n  body.theme-light{\r\n    --bg: #f8fafc;\r\n    --bg-soft: #ffffff;\r\n    --card: rgba(20,20,20,0.04);\r\n    --card-strong: rgba(20,20,20,0.08);\r\n    --text: #0b1220;\r\n    --muted: #51607a;\r\n    --accent: #2563eb;\r\n    --accent2: #7c3aed;\r\n    --border: rgba(10,10,10,0.12);\r\n    --shadow: 0 10px 30px rgba(0,0,0,0.08);\r\n  }\r\n  *{box-sizing:border-box}\r\n  html,body{height:100%}\r\n  body{\r\n    margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, \"Noto Sans KR\",\"Noto Sans\", Arial, Apple SD Gothic Neo, \"Segoe UI Emoji\", \"Segoe UI Symbol\";\r\n    background: radial-gradient(1200px 800px at 80% -20%, rgba(78,140,255,0.18), transparent 50%) , var(--bg);\r\n    color:var(--text);\r\n  }\r\n  .wl-app{max-width:1100px; margin:0 auto; padding:10px 14px 60px}\r\n  .wl-topbar{\r\n    position:sticky; top:0; z-index:20; backdrop-filter:saturate(160%) blur(10px);\r\n    background: linear-gradient(180deg, rgba(0,0,0,0.25), transparent 70%);\r\n    padding:10px 0 12px; margin:0 -14px;\r\n  }\r\n  .wl-topbar-inner{\r\n    max-width:1100px; margin:0 auto; padding:6px 14px; display:flex; align-items:center; gap:10px; justify-content:space-between;\r\n  }\r\n  .wl-brand{\r\n    display:flex; align-items:center; gap:10px; font-weight:800; letter-spacing:.2px;\r\n  }\r\n  .wl-brand-logo{\r\n    width:34px;height:34px;border-radius:10px;\r\n    background: conic-gradient(from 130deg, var(--accent), var(--accent2));\r\n    box-shadow:0 6px 20px rgba(78,140,255,.45), inset 0 0 20px rgba(255,255,255,.2);\r\n  }\r\n  .wl-actions{display:flex; align-items:center; gap:8px; flex-wrap:wrap}\r\n  .wl-select, .wl-btn, .wl-input, .wl-date, .wl-number{\r\n    background: var(--bg-soft); border:1px solid var(--border); color:var(--text);\r\n    padding:10px 12px; border-radius:12px; outline:none; transition:.2s border, .2s background;\r\n  }\r\n  .wl-btn{cursor:pointer}\r\n  .wl-btn:disabled{opacity:.6; cursor:not-allowed}\r\n  .wl-btn.primary{background: linear-gradient(90deg, var(--accent), var(--accent2)); color:white; border:none; box-shadow: var(--shadow)}\r\n  .wl-btn.ghost{background:transparent; border:1px dashed var(--border); color:var(--muted)}\r\n  .wl-badge{font-size:12px; color:var(--muted)}\r\n  .wl-section{margin-top:14px}\r\n  .wl-row{display:grid; gap:12px}\r\n  @media(min-width:840px){\r\n    .wl-row.cols-2{grid-template-columns:1.2fr 1fr}\r\n    .wl-row.cols-3{grid-template-columns:repeat(3,1fr)}\r\n  }\r\n  .wl-card{\r\n    background: linear-gradient(180deg, var(--card), transparent 120%);\r\n    border:1px solid var(--border); border-radius: var(--radius);\r\n    padding:14px; box-shadow: var(--shadow);\r\n  }\r\n  .wl-card h3{margin:0 0 8px}\r\n  .wl-sub{color:var(--muted); font-size:12px}\r\n  .wl-kpi{\r\n    display:flex; gap:10px; flex-wrap:wrap\r\n  }\r\n  .wl-kpi .kpi{\r\n    flex:1 1 220px; padding:16px; border-radius:14px; background: linear-gradient(180deg, var(--card-strong), transparent 120%); border:1px solid var(--border);\r\n  }\r\n  .kpi .label{color:var(--muted); font-size:12px}\r\n  .kpi .value{font-weight:800; font-size:22px; margin-top:6px}\r\n  .kpi .delta{font-size:12px; margin-top:4px}\r\n  .delta.good{color:var(--good)} .delta.bad{color:var(--bad)}\r\n  .wl-quick{\r\n    position:sticky; top:62px; z-index:15; backdrop-filter:saturate(160%) blur(10px);\r\n    border:1px solid var(--border); border-radius:16px; padding:10px; margin-top:10px;\r\n    background: linear-gradient(180deg, var(--card), transparent 120%), var(--bg-soft);\r\n  }\r\n  @media(min-width:840px){ .wl-quick{position:static; top:auto}}\r\n  .wl-seg{\r\n    display:inline-flex; border-radius:12px; border:1px solid var(--border); overflow:hidden\r\n  }\r\n  .wl-seg button{\r\n    padding:10px 14px; background:transparent; color:var(--text); border:none; cursor:pointer; min-width:80px\r\n  }\r\n  .wl-seg button.active{background:linear-gradient(90deg, var(--accent), var(--accent2)); color:white}\r\n  .wl-form{display:grid; gap:10px; grid-template-columns:1fr}\r\n  @media(min-width:640px){\r\n    .wl-form{grid-template-columns:repeat(6,1fr)}\r\n    .f-type{grid-column: span 2}\r\n    .f-acct{grid-column: span 2}\r\n    .f-amt{grid-column: span 2}\r\n    .f-cat{grid-column: span 2}\r\n    .f-memo{grid-column: span 3}\r\n    .f-date{grid-column: span 1}\r\n    .f-submit{grid-column: span 6}\r\n  }\r\n  .amt-wrap{position:relative}\r\n  .amt-wrap .suffix{position:absolute; right:10px; top:50%; transform:translateY(-50%); color:var(--muted); font-size:12px}\r\n  .wl-progress{\r\n    width:100%; height:10px; background:rgba(255,255,255,0.08); border:1px solid var(--border);\r\n    border-radius:999px; overflow:hidden\r\n  }\r\n  .wl-progress > i{display:block; height:100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); width:0%}\r\n  .wl-charts{display:grid; gap:12px}\r\n  @media(min-width:840px){ .wl-charts{grid-template-columns:1fr 1fr}}\r\n  .chart-canvas{width:100%; height:260px; display:block}\r\n  .wl-table-wrap{overflow:auto; border:1px solid var(--border); border-radius:14px}\r\n  table{width:100%; border-collapse:separate; border-spacing:0}\r\n  thead th{\r\n    position:sticky; top:0; z-index:5; background:var(--bg-soft);\r\n    font-size:12px; color:var(--muted); text-align:left; padding:10px; border-bottom:1px solid var(--border)\r\n  }\r\n  tbody td{padding:10px; border-bottom:1px solid var(--border); font-size:14px}\r\n  tbody tr:hover{background:rgba(255,255,255,0.04)}\r\n  .amt-pos{color:var(--good); font-weight:700}\r\n  .amt-neg{color:var(--bad); font-weight:700}\r\n  .wl-filter{display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:8px}\r\n  .wl-foot{margin-top:18px; display:flex; align-items:center; gap:10px; justify-content:space-between; color:var(--muted); font-size:12px}\r\n  .wl-tag{padding:4px 8px; border:1px solid var(--border); border-radius:999px; color:var(--muted)}\r\n  .wl-toast{\r\n    position:fixed; left:50%; bottom:18px; transform:translateX(-50%) translateY(30px);\r\n    padding:10px 14px; border-radius:12px; background:var(--bg-soft); border:1px solid var(--border);\r\n    color:var(--text); opacity:0; transition:.25s; box-shadow:var(--shadow); z-index:50\r\n  }\r\n  .wl-toast.show{opacity:1; transform:translateX(-50%) translateY(0)}\r\n  .danger{border-color: rgba(239,68,68,0.5) !important}\r\n  .wl-inline{display:flex; gap:8px; align-items:center; flex-wrap:wrap}\r\n  .wl-right{margin-left:auto}\r\n  .wl-danger-zone{border:1px dashed rgba(239,68,68,0.5); padding:12px; border-radius:12px}\r\n  .pill{padding:4px 10px; border-radius:999px; background:rgba(255,255,255,0.08); color:var(--muted); font-size:12px}\r\n  .ok-dot{width:8px;height:8px; border-radius:50%; background:var(--good); display:inline-block; margin-right:6px}\r\n  .warn-dot{width:8px;height:8px; border-radius:50%; background:var(--warn); display:inline-block; margin-right:6px}\r\n<\/style>\r\n<\/head>\r\n<body class=\"theme-dark\">\r\n<div class=\"wl-app\" id=\"app\">\r\n  <div class=\"wl-topbar\">\r\n    <div class=\"wl-topbar-inner\">\r\n      <div class=\"wl-brand\">\r\n        <div class=\"wl-brand-logo\" aria-hidden=\"true\"><\/div>\r\n        <div>\r\n          <div style=\"font-size:16px\">Wealth Ledger<\/div>\r\n          <div class=\"wl-badge\" data-i18n=\"subtitle\">VND \uc2e4\uc2dc\uac04 \uac00\uacc4\ubd80<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div class=\"wl-actions\">\r\n        <select id=\"langSelect\" class=\"wl-select\" title=\"Language\">\r\n          <option value=\"ko\">\ud55c\uad6d\uc5b4<\/option>\r\n          <option value=\"vi\">Ti\u1ebfng Vi\u1ec7t<\/option>\r\n        <\/select>\r\n        <button id=\"themeToggle\" class=\"wl-btn\" title=\"Theme\">\ud83c\udf19<\/button>\r\n        <button id=\"exportExcel\" class=\"wl-btn\">Excel<\/button>\r\n        <button id=\"exportCSV\" class=\"wl-btn\">CSV<\/button>\r\n        <button id=\"backupBtn\" class=\"wl-btn\">Backup<\/button>\r\n        <input type=\"file\" id=\"restoreFile\" accept=\"application\/json\" style=\"display:none\">\r\n        <button id=\"restoreBtn\" class=\"wl-btn\">Import<\/button>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <!-- Quick Add (mobile-first sticky) -->\r\n  <div class=\"wl-section wl-quick wl-card\">\r\n    <div class=\"wl-inline\" style=\"justify-content:space-between\">\r\n      <strong data-i18n=\"quickAdd\">\ube60\ub978 \uc785\ub825<\/strong>\r\n      <span class=\"pill\" id=\"integrityPill\"><span class=\"ok-dot\"><\/span><span data-i18n=\"integrityOk\">\ub370\uc774\ud130 \ubb34\uacb0\uc131 \uc815\uc0c1<\/span><\/span>\r\n    <\/div>\r\n    <form id=\"txForm\" class=\"wl-form\" autocomplete=\"off\" action=\"\">\r\n      <div class=\"f-type\">\r\n        <div class=\"wl-seg\" role=\"tablist\" aria-label=\"Type\">\r\n          <button type=\"button\" class=\"seg-btn active\" data-type=\"in\" data-i18n=\"income\">\uc218\uc785<\/button>\r\n          <button type=\"button\" class=\"seg-btn\" data-type=\"out\" data-i18n=\"expense\">\uc9c0\ucd9c<\/button>\r\n        <\/div>\r\n        <input type=\"hidden\" id=\"txType\" value=\"in\">\r\n      <\/div>\r\n      <div class=\"f-acct\">\r\n        <select id=\"txAccount\" class=\"wl-select\" required>\r\n          <option value=\"cash\" data-i18n=\"cash\">\ud604\uae08<\/option>\r\n          <option value=\"biz\" data-i18n=\"biz\">\ubc95\uc778\uacc4\uc88c<\/option>\r\n          <option value=\"personal\" data-i18n=\"personal\">\uac1c\uc778\uacc4\uc88c<\/option>\r\n        <\/select>\r\n      <\/div>\r\n      <div class=\"f-amt\">\r\n        <div class=\"amt-wrap\">\r\n          <input id=\"txAmount\" class=\"wl-input\" type=\"text\" inputmode=\"numeric\" placeholder=\"0\" aria-label=\"Amount\" required>\r\n          <span class=\"suffix\">VND<\/span>\r\n        <\/div>\r\n      <\/div>\r\n      <div class=\"f-cat\">\r\n        <input id=\"txCategory\" class=\"wl-input\" list=\"catList\" placeholder=\"\uce74\ud14c\uace0\ub9ac\" data-i18n-ph=\"categoryPH\">\r\n        <datalist id=\"catList\">\r\n          <option value=\"\uae09\uc5ec \/ L\u01b0\u01a1ng\">\r\n          <option value=\"\uc0ac\uc5c5\uc218\uc775 \/ Doanh thu\">\r\n          <option value=\"\uc2dd\ube44 \/ \u0102n u\u1ed1ng\">\r\n          <option value=\"\uad50\ud1b5 \/ Di chuy\u1ec3n\">\r\n          <option value=\"\uc8fc\uac70 \/ Nh\u00e0 \u1edf\">\r\n          <option value=\"\ud1b5\uc2e0 \/ Vi\u1ec5n th\u00f4ng\">\r\n          <option value=\"\uc758\ub8cc \/ Y t\u1ebf\">\r\n          <option value=\"\uad50\uc721 \/ Gi\u00e1o d\u1ee5c\">\r\n          <option value=\"\uc800\ucd95 \/ Ti\u1ebft ki\u1ec7m\">\r\n          <option value=\"\uae30\ud0c0 \/ Kh\u00e1c\">\r\n        <\/datalist>\r\n      <\/div>\r\n      <div class=\"f-memo\">\r\n        <input id=\"txMemo\" class=\"wl-input\" type=\"text\" placeholder=\"\uba54\ubaa8 (\uc120\ud0dd)\" data-i18n-ph=\"memoPH\">\r\n      <\/div>\r\n      <div class=\"f-date\">\r\n        <input id=\"txDate\" class=\"wl-date\" type=\"date\" required>\r\n      <\/div>\r\n      <div class=\"f-submit\">\r\n        <button class=\"wl-btn primary\" type=\"submit\" style=\"width:100%\" data-i18n=\"add\">\ucd94\uac00<\/button>\r\n      <\/div>\r\n    <input type=\"hidden\" name=\"trp-form-language\" value=\"en\"\/><\/form>\r\n  <\/div>\r\n\r\n  <!-- Summary -->\r\n  <div class=\"wl-section\">\r\n    <div class=\"wl-kpi\">\r\n      <div class=\"kpi\">\r\n        <div class=\"label\" data-i18n=\"netWorth\">\ucd1d\uc790\uc0b0<\/div>\r\n        <div class=\"value\" id=\"kpiNet\">\u20ab0<\/div>\r\n        <div class=\"delta\" id=\"kpiSavings\"><span data-i18n=\"thisMonthSavings\">\uc774\ubc88\ub2ec \uc800\ucd95<\/span>: <b>\u20ab0<\/b><\/div>\r\n      <\/div>\r\n      <div class=\"kpi\">\r\n        <div class=\"label\" data-i18n=\"cash\">\ud604\uae08<\/div>\r\n        <div class=\"value\" id=\"kpiCash\">\u20ab0<\/div>\r\n        <div class=\"delta sub\" id=\"kpiCashDelta\"><\/div>\r\n      <\/div>\r\n      <div class=\"kpi\">\r\n        <div class=\"label\" data-i18n=\"biz\">\ubc95\uc778\uacc4\uc88c<\/div>\r\n        <div class=\"value\" id=\"kpiBiz\">\u20ab0<\/div>\r\n        <div class=\"delta sub\" id=\"kpiBizDelta\"><\/div>\r\n      <\/div>\r\n      <div class=\"kpi\">\r\n        <div class=\"label\" data-i18n=\"personal\">\uac1c\uc778\uacc4\uc88c<\/div>\r\n        <div class=\"value\" id=\"kpiPersonal\">\u20ab0<\/div>\r\n        <div class=\"delta sub\" id=\"kpiPersonalDelta\"><\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <!-- Budgets -->\r\n  <div class=\"wl-section wl-row cols-2\">\r\n    <div class=\"wl-card\">\r\n      <h3 data-i18n=\"budgets\">\uc9c0\ucd9c \uacc4\ud68d<\/h3>\r\n      <div class=\"wl-row\">\r\n        <div>\r\n          <div class=\"wl-inline\" style=\"justify-content:space-between\">\r\n            <div><b data-i18n=\"weeklyPlan\">\uc8fc\uac04 \uc9c0\ucd9c \uacc4\ud68d<\/b><\/div>\r\n            <div class=\"wl-inline\"><span id=\"weeklyActual\">\u20ab0<\/span> \/ <span id=\"weeklyPlan\">\u20ab0<\/span><\/div>\r\n          <\/div>\r\n          <div class=\"wl-progress\" aria-label=\"Weekly progress\"><i id=\"progWeekly\" style=\"width:0%\"><\/i><\/div>\r\n        <\/div>\r\n        <div>\r\n          <div class=\"wl-inline\" style=\"justify-content:space-between\">\r\n            <div><b data-i18n=\"monthlyPlan\">\uc6d4\uac04 \uc9c0\ucd9c \uacc4\ud68d<\/b><\/div>\r\n            <div class=\"wl-inline\"><span id=\"monthlyActual\">\u20ab0<\/span> \/ <span id=\"monthlyPlan\">\u20ab0<\/span><\/div>\r\n          <\/div>\r\n          <div class=\"wl-progress\" aria-label=\"Monthly progress\"><i id=\"progMonthly\" style=\"width:0%\"><\/i><\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div class=\"wl-inline\" style=\"margin-top:10px\">\r\n        <div class=\"amt-wrap\">\r\n          <input id=\"inpWeeklyPlan\" class=\"wl-input\" type=\"text\" inputmode=\"numeric\" placeholder=\"0\">\r\n          <span class=\"suffix\">VND<\/span>\r\n        <\/div>\r\n        <div class=\"amt-wrap\">\r\n          <input id=\"inpMonthlyPlan\" class=\"wl-input\" type=\"text\" inputmode=\"numeric\" placeholder=\"0\">\r\n          <span class=\"suffix\">VND<\/span>\r\n        <\/div>\r\n        <button id=\"saveBudgets\" class=\"wl-btn primary\"><span data-i18n=\"save\">\uc800\uc7a5<\/span><\/button>\r\n      <\/div>\r\n      <div class=\"wl-sub\" style=\"margin-top:6px\" data-i18n=\"budgetHint\">\uacc4\ud68d \ub300\ube44 \uc2e4\uc81c \uc9c0\ucd9c\uc744 \uc904\uc5ec \uc800\ucd95\uc744 \ub298\ub824\ubcf4\uc138\uc694.<\/div>\r\n    <\/div>\r\n\r\n    <div class=\"wl-card\">\r\n      <h3 data-i18n=\"initialBalances\">\ucd08\uae30 \uc794\uc561<\/h3>\r\n      <div class=\"wl-row\">\r\n        <div class=\"amt-wrap\">\r\n          <label class=\"wl-sub\" data-i18n=\"cash\">\ud604\uae08<\/label>\r\n          <input id=\"initCash\" class=\"wl-input\" type=\"text\" inputmode=\"numeric\" placeholder=\"0\"><span class=\"suffix\">VND<\/span>\r\n        <\/div>\r\n        <div class=\"amt-wrap\">\r\n          <label class=\"wl-sub\" data-i18n=\"biz\">\ubc95\uc778\uacc4\uc88c<\/label>\r\n          <input id=\"initBiz\" class=\"wl-input\" type=\"text\" inputmode=\"numeric\" placeholder=\"0\"><span class=\"suffix\">VND<\/span>\r\n        <\/div>\r\n        <div class=\"amt-wrap\">\r\n          <label class=\"wl-sub\" data-i18n=\"personal\">\uac1c\uc778\uacc4\uc88c<\/label>\r\n          <input id=\"initPersonal\" class=\"wl-input\" type=\"text\" inputmode=\"numeric\" placeholder=\"0\"><span class=\"suffix\">VND<\/span>\r\n        <\/div>\r\n      <\/div>\r\n      <div class=\"wl-inline\" style=\"margin-top:10px\">\r\n        <button id=\"saveInit\" class=\"wl-btn primary\"><span data-i18n=\"save\">\uc800\uc7a5<\/span><\/button>\r\n        <span class=\"wl-sub\" data-i18n=\"initNote\">\ucd08\uae30 \uc794\uc561\uc740 \ub204\uc801 \ud569\uacc4\uc5d0 \uc989\uc2dc \ubc18\uc601\ub429\ub2c8\ub2e4.<\/span>\r\n      <\/div>\r\n      <div class=\"wl-danger-zone\" style=\"margin-top:10px\">\r\n        <div class=\"wl-inline\">\r\n          <button id=\"resetAll\" class=\"wl-btn danger\"><span data-i18n=\"resetAll\">\ubaa8\ub4e0 \ub370\uc774\ud130 \ucd08\uae30\ud654<\/span><\/button>\r\n          <span class=\"wl-sub\" data-i18n=\"resetHint\">\ub418\ub3cc\ub9b4 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ubc31\uc5c5\uc744 \uad8c\uc7a5\ud569\ub2c8\ub2e4.<\/span>\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <!-- Charts -->\r\n  <div class=\"wl-section wl-charts\">\r\n    <div class=\"wl-card\">\r\n      <h3 data-i18n=\"chartIncomeExpense\">\ucd5c\uadfc 12\uac1c\uc6d4 \uc218\uc785\/\uc9c0\ucd9c<\/h3>\r\n      <canvas id=\"chartIE\" class=\"chart-canvas\" aria-label=\"Income vs Expense chart\"><\/canvas>\r\n    <\/div>\r\n    <div class=\"wl-card\">\r\n      <h3 data-i18n=\"chartSavings\">\uc6d4\ubcc4 \uc800\ucd95<\/h3>\r\n      <canvas id=\"chartSavings\" class=\"chart-canvas\" aria-label=\"Savings chart\"><\/canvas>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <!-- Transactions -->\r\n  <div class=\"wl-section wl-card\">\r\n    <div class=\"wl-inline\" style=\"justify-content:space-between\">\r\n      <h3 data-i18n=\"transactions\">\uac70\ub798 \ub0b4\uc5ed<\/h3>\r\n      <div class=\"wl-filter\">\r\n        <select id=\"fltAccount\" class=\"wl-select\">\r\n          <option value=\"all\" data-i18n=\"all\">\uc804\uccb4<\/option>\r\n          <option value=\"cash\" data-i18n=\"cash\">\ud604\uae08<\/option>\r\n          <option value=\"biz\" data-i18n=\"biz\">\ubc95\uc778\uacc4\uc88c<\/option>\r\n          <option value=\"personal\" data-i18n=\"personal\">\uac1c\uc778\uacc4\uc88c<\/option>\r\n        <\/select>\r\n        <select id=\"fltType\" class=\"wl-select\">\r\n          <option value=\"all\" data-i18n=\"all\">\uc804\uccb4<\/option>\r\n          <option value=\"in\" data-i18n=\"income\">\uc218\uc785<\/option>\r\n          <option value=\"out\" data-i18n=\"expense\">\uc9c0\ucd9c<\/option>\r\n        <\/select>\r\n        <input id=\"fltMonth\" class=\"wl-date\" type=\"month\">\r\n        <input id=\"fltSearch\" class=\"wl-input\" type=\"text\" placeholder=\"\uac80\uc0c9\">\r\n      <\/div>\r\n    <\/div>\r\n    <div class=\"wl-table-wrap\">\r\n      <table>\r\n        <thead>\r\n          <tr>\r\n            <th data-i18n=\"date\">\ub0a0\uc9dc<\/th>\r\n            <th data-i18n=\"account\">\uacc4\uc815<\/th>\r\n            <th data-i18n=\"type\">\uc720\ud615<\/th>\r\n            <th data-i18n=\"category\">\uce74\ud14c\uace0\ub9ac<\/th>\r\n            <th data-i18n=\"memo\">\uba54\ubaa8<\/th>\r\n            <th data-i18n=\"amount\">\uae08\uc561<\/th>\r\n            <th data-i18n=\"actions\">\uc791\uc5c5<\/th>\r\n          <\/tr>\r\n        <\/thead>\r\n        <tbody id=\"txTbody\"><\/tbody>\r\n      <\/table>\r\n    <\/div>\r\n    <div class=\"wl-foot\">\r\n      <div><span class=\"wl-tag\" id=\"txCount\">0<\/span> <span data-i18n=\"rows\">case<\/span><\/div>\r\n      <div class=\"wl-inline\">\r\n        <div class=\"wl-tag\"><span data-i18n=\"income\">\uc218\uc785<\/span>: <b id=\"sumIn\">\u20ab0<\/b><\/div>\r\n        <div class=\"wl-tag\"><span data-i18n=\"expense\">\uc9c0\ucd9c<\/span>: <b id=\"sumOut\">\u20ab0<\/b><\/div>\r\n        <div class=\"wl-tag\"><span data-i18n=\"savings\">\uc800\ucd95<\/span>: <b id=\"sumSav\">\u20ab0<\/b><\/div>\r\n      <\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <div class=\"wl-foot\">\r\n    <div class=\"wl-inline\">\r\n      <span class=\"wl-sub\" data-i18n=\"footerHint\">\uc2b5\uad00\uc774 \ubd80\ub97c \ub9cc\ub4ed\ub2c8\ub2e4. \uc624\ub298\ub3c4 \uae30\ub85d\ud558\uc138\uc694 \u2728<\/span>\r\n    <\/div>\r\n    <div class=\"wl-inline\">\r\n      <span class=\"wl-sub\">VND<\/span>\r\n    <\/div>\r\n  <\/div>\r\n\r\n<\/div>\r\n\r\n<div id=\"toast\" class=\"wl-toast\" role=\"status\" aria-live=\"polite\"><\/div>\r\n\r\n<script>\r\n\/* ===========================\r\n   Wealth Ledger (VND) \u2014 No external libs. LocalStorage only.\r\n   Accurate integer math for VND. Mobile-first quick input. Bilingual KO\/VI.\r\n   =========================== *\/\r\n\r\n(function(){\r\n  \"use strict\";\r\n\r\n  \/\/ Storage keys and version\r\n  const K = {\r\n    TX: 'wl_tx_v1',\r\n    ST: 'wl_settings_v1'\r\n  };\r\n\r\n  \/\/ i18n dictionary\r\n  const I18N = {\r\n    ko: {\r\n      subtitle: \"VND \uc2e4\uc2dc\uac04 \uac00\uacc4\ubd80\",\r\n      quickAdd: \"\ube60\ub978 \uc785\ub825\",\r\n      integrityOk: \"\ub370\uc774\ud130 \ubb34\uacb0\uc131 \uc815\uc0c1\",\r\n      integrityWarn: \"\uc810\uac80 \ud544\uc694\",\r\n      income: \"\uc218\uc785\",\r\n      expense: \"\uc9c0\ucd9c\",\r\n      add: \"\ucd94\uac00\",\r\n      netWorth: \"\ucd1d\uc790\uc0b0\",\r\n      thisMonthSavings: \"\uc774\ubc88\ub2ec \uc800\ucd95\",\r\n      cash: \"\ud604\uae08\",\r\n      biz: \"\ubc95\uc778\uacc4\uc88c\",\r\n      personal: \"\uac1c\uc778\uacc4\uc88c\",\r\n      budgets: \"\uc9c0\ucd9c \uacc4\ud68d\",\r\n      weeklyPlan: \"\uc8fc\uac04 \uc9c0\ucd9c \uacc4\ud68d\",\r\n      monthlyPlan: \"\uc6d4\uac04 \uc9c0\ucd9c \uacc4\ud68d\",\r\n      save: \"\uc800\uc7a5\",\r\n      budgetHint: \"\uacc4\ud68d \ub300\ube44 \uc2e4\uc81c \uc9c0\ucd9c\uc744 \uc904\uc5ec \uc800\ucd95\uc744 \ub298\ub824\ubcf4\uc138\uc694.\",\r\n      initialBalances: \"\ucd08\uae30 \uc794\uc561\",\r\n      initNote: \"\ucd08\uae30 \uc794\uc561\uc740 \ub204\uc801 \ud569\uacc4\uc5d0 \uc989\uc2dc \ubc18\uc601\ub429\ub2c8\ub2e4.\",\r\n      resetAll: \"\ubaa8\ub4e0 \ub370\uc774\ud130 \ucd08\uae30\ud654\",\r\n      resetHint: \"\ub418\ub3cc\ub9b4 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ubc31\uc5c5\uc744 \uad8c\uc7a5\ud569\ub2c8\ub2e4.\",\r\n      chartIncomeExpense: \"\ucd5c\uadfc 12\uac1c\uc6d4 \uc218\uc785\/\uc9c0\ucd9c\",\r\n      chartSavings: \"\uc6d4\ubcc4 \uc800\ucd95\",\r\n      transactions: \"\uac70\ub798 \ub0b4\uc5ed\",\r\n      all: \"\uc804\uccb4\",\r\n      date: \"\ub0a0\uc9dc\",\r\n      account: \"\uacc4\uc815\",\r\n      type: \"\uc720\ud615\",\r\n      category: \"\uce74\ud14c\uace0\ub9ac\",\r\n      memo: \"\uba54\ubaa8\",\r\n      amount: \"\uae08\uc561\",\r\n      actions: \"\uc791\uc5c5\",\r\n      rows: \"\uac74\",\r\n      savings: \"\uc800\ucd95\",\r\n      footerHint: \"\uc2b5\uad00\uc774 \ubd80\ub97c \ub9cc\ub4ed\ub2c8\ub2e4. \uc624\ub298\ub3c4 \uae30\ub85d\ud558\uc138\uc694 \u2728\",\r\n      categoryPH: \"\uce74\ud14c\uace0\ub9ac\",\r\n      memoPH: \"\uba54\ubaa8 (\uc120\ud0dd)\",\r\n      btnDelete: \"\uc0ad\uc81c\",\r\n      confirmDelete: \"\uc0ad\uc81c\ud560\uae4c\uc694?\",\r\n      confirmReset: \"\uc815\ub9d0 \ubaa8\ub4e0 \ub370\uc774\ud130\ub97c \ucd08\uae30\ud654\ud558\uc2dc\uaca0\uc5b4\uc694? \uc774 \uc791\uc5c5\uc740 \ub418\ub3cc\ub9b4 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.\",\r\n      saved: \"\uc800\uc7a5\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\",\r\n      added: \"\ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\",\r\n      deleted: \"\uc0ad\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\",\r\n      resetDone: \"\ucd08\uae30\ud654\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\",\r\n      imported: \"\ubcf5\uc6d0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.\",\r\n      invalidAmount: \"\uae08\uc561\uc744 \uc785\ub825\ud558\uc138\uc694.\",\r\n      invalidDate: \"\ub0a0\uc9dc\ub97c \uc785\ub825\ud558\uc138\uc694.\",\r\n      invalidAccount: \"\uacc4\uc815\uc744 \uc120\ud0dd\ud558\uc138\uc694.\"\r\n    },\r\n    vi: {\r\n      subtitle: \"S\u1ed5 chi ti\u00eau VND th\u1eddi gian th\u1ef1c\",\r\n      quickAdd: \"Nh\u1eadp nhanh\",\r\n      integrityOk: \"D\u1eef li\u1ec7u \u1ed5n \u0111\u1ecbnh\",\r\n      integrityWarn: \"C\u1ea7n ki\u1ec3m tra\",\r\n      income: \"Thu\",\r\n      expense: \"Chi\",\r\n      add: \"Th\u00eam\",\r\n      netWorth: \"T\u1ed5ng t\u00e0i s\u1ea3n\",\r\n      thisMonthSavings: \"Ti\u1ebft ki\u1ec7m th\u00e1ng n\u00e0y\",\r\n      cash: \"Ti\u1ec1n m\u1eb7t\",\r\n      biz: \"T\u00e0i kho\u1ea3n c\u00f4ng ty\",\r\n      personal: \"T\u00e0i kho\u1ea3n c\u00e1 nh\u00e2n\",\r\n      budgets: \"K\u1ebf ho\u1ea1ch chi\",\r\n      weeklyPlan: \"K\u1ebf ho\u1ea1ch chi tu\u1ea7n\",\r\n      monthlyPlan: \"K\u1ebf ho\u1ea1ch chi th\u00e1ng\",\r\n      save: \"L\u01b0u\",\r\n      budgetHint: \"Gi\u1ea3m chi so v\u1edbi k\u1ebf ho\u1ea1ch \u0111\u1ec3 t\u0103ng ti\u1ebft ki\u1ec7m.\",\r\n      initialBalances: \"S\u1ed1 d\u01b0 ban \u0111\u1ea7u\",\r\n      initNote: \"S\u1ed1 d\u01b0 ban \u0111\u1ea7u \u0111\u01b0\u1ee3c \u00e1p d\u1ee5ng ngay.\",\r\n      resetAll: \"\u0110\u1eb7t l\u1ea1i to\u00e0n b\u1ed9 d\u1eef li\u1ec7u\",\r\n      resetHint: \"Kh\u00f4ng th\u1ec3 ho\u00e0n t\u00e1c. H\u00e3y sao l\u01b0u tr\u01b0\u1edbc.\",\r\n      chartIncomeExpense: \"Thu\/Chi 12 th\u00e1ng g\u1ea7n \u0111\u00e2y\",\r\n      chartSavings: \"Ti\u1ebft ki\u1ec7m theo th\u00e1ng\",\r\n      transactions: \"Giao d\u1ecbch\",\r\n      all: \"T\u1ea5t c\u1ea3\",\r\n      date: \"Ng\u00e0y\",\r\n      account: \"T\u00e0i kho\u1ea3n\",\r\n      type: \"Lo\u1ea1i\",\r\n      category: \"Danh m\u1ee5c\",\r\n      memo: \"Ghi ch\u00fa\",\r\n      amount: \"S\u1ed1 ti\u1ec1n\",\r\n      actions: \"Thao t\u00e1c\",\r\n      rows: \"d\u00f2ng\",\r\n      savings: \"Ti\u1ebft ki\u1ec7m\",\r\n      footerHint: \"Th\u00f3i quen t\u1ea1o n\u00ean s\u1ef1 gi\u00e0u c\u00f3. Ghi l\u1ea1i h\u00f4m nay \u2728\",\r\n      categoryPH: \"Danh m\u1ee5c\",\r\n      memoPH: \"Ghi ch\u00fa (tu\u1ef3 ch\u1ecdn)\",\r\n      btnDelete: \"X\u00f3a\",\r\n      confirmDelete: \"X\u00f3a giao d\u1ecbch n\u00e0y?\",\r\n      confirmReset: \"B\u1ea1n c\u00f3 ch\u1eafc mu\u1ed1n \u0111\u1eb7t l\u1ea1i to\u00e0n b\u1ed9 d\u1eef li\u1ec7u? Kh\u00f4ng th\u1ec3 ho\u00e0n t\u00e1c.\",\r\n      saved: \"\u0110\u00e3 l\u01b0u.\",\r\n      added: \"\u0110\u00e3 th\u00eam.\",\r\n      deleted: \"\u0110\u00e3 x\u00f3a.\",\r\n      resetDone: \"\u0110\u00e3 \u0111\u1eb7t l\u1ea1i.\",\r\n      imported: \"\u0110\u00e3 kh\u00f4i ph\u1ee5c.\",\r\n      invalidAmount: \"H\u00e3y nh\u1eadp s\u1ed1 ti\u1ec1n.\",\r\n      invalidDate: \"H\u00e3y nh\u1eadp ng\u00e0y.\",\r\n      invalidAccount: \"H\u00e3y ch\u1ecdn t\u00e0i kho\u1ea3n.\"\r\n    }\r\n  };\r\n\r\n  \/\/ Default settings\r\n  const defaultSettings = () => ({\r\n    lang: 'ko',\r\n    theme: 'dark',\r\n    budgets: { weekly: 0, monthly: 0 },\r\n    initBalances: { cash: 0, biz: 0, personal: 0 }\r\n  });\r\n\r\n  \/\/ State\r\n  let settings = loadSettings();\r\n  let tx = loadTx();\r\n\r\n  \/\/ Elements\r\n  const $ = (s, p=document) => p.querySelector(s);\r\n  const $$ = (s, p=document) => Array.from(p.querySelectorAll(s));\r\n\r\n  const els = {\r\n    langSelect: $('#langSelect'),\r\n    themeToggle: $('#themeToggle'),\r\n    exportExcel: $('#exportExcel'),\r\n    exportCSV: $('#exportCSV'),\r\n    backupBtn: $('#backupBtn'),\r\n    restoreBtn: $('#restoreBtn'),\r\n    restoreFile: $('#restoreFile'),\r\n    integrityPill: $('#integrityPill'),\r\n    toast: $('#toast'),\r\n\r\n    \/\/ Quick form\r\n    form: $('#txForm'),\r\n    typeSeg: $$('.seg-btn'),\r\n    txType: $('#txType'),\r\n    txAccount: $('#txAccount'),\r\n    txAmount: $('#txAmount'),\r\n    txCategory: $('#txCategory'),\r\n    txMemo: $('#txMemo'),\r\n    txDate: $('#txDate'),\r\n\r\n    \/\/ KPI\r\n    kpiNet: $('#kpiNet'),\r\n    kpiSavings: $('#kpiSavings'),\r\n    kpiCash: $('#kpiCash'),\r\n    kpiBiz: $('#kpiBiz'),\r\n    kpiPersonal: $('#kpiPersonal'),\r\n    kpiCashDelta: $('#kpiCashDelta'),\r\n    kpiBizDelta: $('#kpiBizDelta'),\r\n    kpiPersonalDelta: $('#kpiPersonalDelta'),\r\n\r\n    \/\/ Budgets\r\n    weeklyActual: $('#weeklyActual'),\r\n    weeklyPlan: $('#weeklyPlan'),\r\n    monthlyActual: $('#monthlyActual'),\r\n    monthlyPlan: $('#monthlyPlan'),\r\n    progWeekly: $('#progWeekly'),\r\n    progMonthly: $('#progMonthly'),\r\n    inpWeeklyPlan: $('#inpWeeklyPlan'),\r\n    inpMonthlyPlan: $('#inpMonthlyPlan'),\r\n    saveBudgets: $('#saveBudgets'),\r\n\r\n    \/\/ Init balances\r\n    initCash: $('#initCash'),\r\n    initBiz: $('#initBiz'),\r\n    initPersonal: $('#initPersonal'),\r\n    saveInit: $('#saveInit'),\r\n    resetAll: $('#resetAll'),\r\n\r\n    \/\/ Charts\r\n    chartIE: $('#chartIE'),\r\n    chartSavings: $('#chartSavings'),\r\n\r\n    \/\/ Transactions\r\n    fltAccount: $('#fltAccount'),\r\n    fltType: $('#fltType'),\r\n    fltMonth: $('#fltMonth'),\r\n    fltSearch: $('#fltSearch'),\r\n    txTbody: $('#txTbody'),\r\n    txCount: $('#txCount'),\r\n    sumIn: $('#sumIn'),\r\n    sumOut: $('#sumOut'),\r\n    sumSav: $('#sumSav'),\r\n  };\r\n\r\n  \/\/ Init language and theme\r\n  els.langSelect.value = settings.lang;\r\n  applyI18N();\r\n  applyTheme(settings.theme);\r\n\r\n  \/\/ Set default date as today\r\n  els.txDate.value = fmtDateInput(new Date());\r\n\r\n  \/\/ Fill inputs\r\n  setAmtInput(els.inpWeeklyPlan, settings.budgets.weekly);\r\n  setAmtInput(els.inpMonthlyPlan, settings.budgets.monthly);\r\n  setAmtInput(els.initCash, settings.initBalances.cash);\r\n  setAmtInput(els.initBiz, settings.initBalances.biz);\r\n  setAmtInput(els.initPersonal, settings.initBalances.personal);\r\n\r\n  \/\/ Bind events\r\n  els.typeSeg.forEach(b=>{\r\n    b.addEventListener('click', ()=>{\r\n      els.typeSeg.forEach(x=>x.classList.remove('active'));\r\n      b.classList.add('active');\r\n      els.txType.value = b.dataset.type;\r\n    });\r\n  });\r\n\r\n  \/\/ amount format on typing\r\n  [els.txAmount, els.inpWeeklyPlan, els.inpMonthlyPlan, els.initCash, els.initBiz, els.initPersonal].forEach(inp=>{\r\n    inp.addEventListener('input', ()=> formatAmountInput(inp));\r\n    \/\/ keep caret at end on mobile is fine\r\n  });\r\n\r\n  els.form.addEventListener('submit', onAddTx);\r\n  els.saveBudgets.addEventListener('click', onSaveBudgets);\r\n  els.saveInit.addEventListener('click', onSaveInit);\r\n  els.resetAll.addEventListener('click', onReset);\r\n\r\n  els.langSelect.addEventListener('change', ()=>{\r\n    settings.lang = els.langSelect.value;\r\n    saveSettings();\r\n    applyI18N();\r\n    drawAll();\r\n  });\r\n\r\n  els.themeToggle.addEventListener('click', ()=>{\r\n    settings.theme = (settings.theme==='dark'?'light':'dark');\r\n    saveSettings();\r\n    applyTheme(settings.theme);\r\n    drawAll();\r\n  });\r\n\r\n  els.exportExcel.addEventListener('click', ()=> exportExcel());\r\n  els.exportCSV.addEventListener('click', ()=> exportCSV());\r\n  els.backupBtn.addEventListener('click', ()=> backupJSON());\r\n  els.restoreBtn.addEventListener('click', ()=> els.restoreFile.click());\r\n  els.restoreFile.addEventListener('change', restoreJSON);\r\n\r\n  \/\/ Filters\r\n  [els.fltAccount, els.fltType, els.fltMonth, els.fltSearch].forEach(el=>{\r\n    el.addEventListener('input', ()=> renderTable());\r\n  });\r\n\r\n  \/\/ Resize charts\r\n  window.addEventListener('resize', debounce(drawAll, 200), {passive:true});\r\n\r\n  \/\/ Initial render\r\n  drawAll();\r\n\r\n  \/\/ -------------- Functions --------------\r\n\r\n  function onAddTx(e){\r\n    e.preventDefault();\r\n    \/\/ Validate\r\n    const type = els.txType.value; \/\/ 'in' or 'out'\r\n    const account = els.txAccount.value;\r\n    const amt = parseAmt(els.txAmount.value);\r\n    const date = els.txDate.value;\r\n    if(!amt || amt<=0){ return alertI18n('invalidAmount'); }\r\n    if(!date){ return alertI18n('invalidDate'); }\r\n    if(!account){ return alertI18n('invalidAccount'); }\r\n    const category = (els.txCategory.value||'').trim();\r\n    const memo = (els.txMemo.value||'').trim();\r\n\r\n    const t = {\r\n      id: uid(),\r\n      type, account,\r\n      amount: amt|0,\r\n      date,\r\n      category,\r\n      memo,\r\n      createdAt: Date.now()\r\n    };\r\n    tx.push(t);\r\n    saveTx();\r\n\r\n    \/\/ Clear amount, memo only (keep other for speed)\r\n    els.txAmount.value = '';\r\n    els.txMemo.value = '';\r\n    showToast(t(type==='in'?'added':'added')); \/\/ same message\r\n\r\n    drawAll();\r\n  }\r\n\r\n  function onSaveBudgets(){\r\n    settings.budgets.weekly = parseAmt(els.inpWeeklyPlan.value);\r\n    settings.budgets.monthly = parseAmt(els.inpMonthlyPlan.value);\r\n    saveSettings();\r\n    showToast(t('saved'));\r\n    drawAll();\r\n  }\r\n\r\n  function onSaveInit(){\r\n    settings.initBalances.cash = parseAmt(els.initCash.value);\r\n    settings.initBalances.biz = parseAmt(els.initBiz.value);\r\n    settings.initBalances.personal = parseAmt(els.initPersonal.value);\r\n    saveSettings();\r\n    showToast(t('saved'));\r\n    drawAll();\r\n  }\r\n\r\n  function onReset(){\r\n    if(!confirm(t('confirmReset'))) return;\r\n    localStorage.removeItem(K.TX);\r\n    localStorage.removeItem(K.ST);\r\n    settings = defaultSettings();\r\n    tx = [];\r\n    els.langSelect.value = settings.lang;\r\n    applyI18N();\r\n    applyTheme(settings.theme);\r\n    els.txDate.value = fmtDateInput(new Date());\r\n    [els.inpWeeklyPlan, els.inpMonthlyPlan, els.initCash, els.initBiz, els.initPersonal].forEach(i=> i.value = '');\r\n    drawAll();\r\n    showToast(t('resetDone'));\r\n  }\r\n\r\n  \/\/ Storage helpers\r\n  function loadSettings(){\r\n    try{\r\n      const raw = localStorage.getItem(K.ST);\r\n      if(!raw) return defaultSettings();\r\n      const s = JSON.parse(raw);\r\n      \/\/ merge defaults\r\n      const d = defaultSettings();\r\n      return {\r\n        lang: s.lang || d.lang,\r\n        theme: s.theme || d.theme,\r\n        budgets: {\r\n          weekly: toInt((s.budgets&&s.budgets.weekly)||0),\r\n          monthly: toInt((s.budgets&&s.budgets.monthly)||0)\r\n        },\r\n        initBalances: {\r\n          cash: toInt((s.initBalances&&s.initBalances.cash)||0),\r\n          biz: toInt((s.initBalances&&s.initBalances.biz)||0),\r\n          personal: toInt((s.initBalances&&s.initBalances.personal)||0),\r\n        }\r\n      };\r\n    }catch(e){\r\n      console.warn('Settings load failed', e);\r\n      return defaultSettings();\r\n    }\r\n  }\r\n  function saveSettings(){\r\n    localStorage.setItem(K.ST, JSON.stringify(settings));\r\n  }\r\n  function loadTx(){\r\n    try{\r\n      const raw = localStorage.getItem(K.TX);\r\n      if(!raw) return [];\r\n      const arr = JSON.parse(raw);\r\n      return arr.map(n=>({\r\n        id: n.id || uid(),\r\n        type: n.type==='in'?'in':'out',\r\n        account: ['cash','biz','personal'].includes(n.account)?n.account:'cash',\r\n        amount: toInt(n.amount||0),\r\n        date: n.date || fmtDateInput(new Date()),\r\n        category: n.category || '',\r\n        memo: n.memo || '',\r\n        createdAt: n.createdAt || Date.now()\r\n      }));\r\n    }catch(e){\r\n      console.warn('Tx load failed', e);\r\n      return [];\r\n    }\r\n  }\r\n  function saveTx(){\r\n    localStorage.setItem(K.TX, JSON.stringify(tx));\r\n  }\r\n\r\n  \/\/ Formatting\r\n  function toInt(n){\r\n    n = Number(n)||0;\r\n    return Math.round(n);\r\n  }\r\n  function parseAmt(str){\r\n    if(typeof str==='number') return Math.round(str);\r\n    const digits = (str||'').toString().replace(\/[^\\d]\/g,'');\r\n    if(!digits) return 0;\r\n    \/\/ Use Number (safe up to 9,007,199,254,740,991)\r\n    return Number(digits);\r\n  }\r\n  function formatVND(n){\r\n    try{\r\n      return new Intl.NumberFormat('vi-VN', {style:'currency', currency:'VND', maximumFractionDigits:0}).format(Math.round(n||0));\r\n    }catch(e){\r\n      const s = (Math.round(n||0)).toString().replace(\/\\B(?=(\\d{3})+(?!\\d))\/g, \",\");\r\n      return '\u20ab'+s;\r\n    }\r\n  }\r\n  function setAmtInput(inp, n){\r\n    inp.value = n ? numberWithCommas(n) : '';\r\n  }\r\n  function formatAmountInput(inp){\r\n    const caretEnd = document.activeElement===inp;\r\n    const val = parseAmt(inp.value);\r\n    inp.value = val ? numberWithCommas(val) : '';\r\n  }\r\n  function numberWithCommas(x){\r\n    const s = (Math.round(x||0)).toString();\r\n    return s.replace(\/\\B(?=(\\d{3})+(?!\\d))\/g, \",\");\r\n  }\r\n  function fmtDateInput(d){\r\n    const y = d.getFullYear();\r\n    const m = String(d.getMonth()+1).padStart(2,'0');\r\n    const day = String(d.getDate()).padStart(2,'0');\r\n    return `${y}-${m}-${day}`;\r\n  }\r\n\r\n  \/\/ i18n\r\n  function t(key){\r\n    const L = I18N[settings.lang] || I18N.ko;\r\n    return L[key] || I18N.ko[key] || key;\r\n  }\r\n  function applyI18N(){\r\n    document.documentElement.lang = settings.lang;\r\n    $$('[data-i18n]').forEach(el=>{\r\n      el.textContent = t(el.dataset.i18n);\r\n    });\r\n    $$('[data-i18n-ph]').forEach(el=>{\r\n      el.setAttribute('placeholder', t(el.dataset.i18nPh));\r\n    });\r\n    \/\/ Update select options text\r\n    \/\/ Already handled by data-i18n in options: refresh by cloning\r\n    $$('option[data-i18n]').forEach(opt=>{\r\n      opt.textContent = t(opt.dataset.i18n);\r\n    });\r\n  }\r\n\r\n  \/\/ Theme\r\n  function applyTheme(mode){\r\n    document.body.classList.toggle('theme-light', mode==='light');\r\n    els.themeToggle.textContent = mode==='light' ? '\ud83c\udf1e' : '\ud83c\udf19';\r\n  }\r\n\r\n  \/\/ Compute\r\n  function computeBalances(){\r\n    const init = settings.initBalances;\r\n    const sum = { cash: init.cash, biz: init.biz, personal: init.personal };\r\n    for(const r of tx){\r\n      const sgn = r.type==='in'?1:-1;\r\n      sum[r.account] += sgn * r.amount;\r\n    }\r\n    return sum;\r\n  }\r\n  function computeMonthlyIE(){\r\n    \/\/ last 12 months inclusive\r\n    const res = [];\r\n    const now = new Date();\r\n    for(let i=11; i>=0; i--){\r\n      const d = new Date(now.getFullYear(), now.getMonth()-i, 1);\r\n      const key = d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0');\r\n      res.push({ key, y: d.getFullYear(), m: d.getMonth()+1, in:0, out:0 });\r\n    }\r\n    const map = Object.fromEntries(res.map(r=>[r.key,r]));\r\n    for(const r of tx){\r\n      const [yy,mm] = r.date.split('-');\r\n      const key = yy+'-'+mm;\r\n      if(map[key]){\r\n        if(r.type==='in') map[key].in += r.amount; else map[key].out += r.amount;\r\n      }\r\n    }\r\n    return res;\r\n  }\r\n  function computeCurrentMonthSavings(){\r\n    const now = new Date();\r\n    const ym = now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0');\r\n    let s=0;\r\n    for(const r of tx){\r\n      if(r.date.startsWith(ym)){\r\n        s += (r.type==='in'?1:-1)*r.amount;\r\n      }\r\n    }\r\n    return s;\r\n  }\r\n  function getWeekRange(d=new Date()){\r\n    \/\/ Monday as first day\r\n    const day = d.getDay(); \/\/ 0 Sun - 6 Sat\r\n    const diffToMon = (day===0? -6 : 1 - day);\r\n    const monday = new Date(d); monday.setDate(d.getDate()+diffToMon);\r\n    const sunday = new Date(monday); sunday.setDate(monday.getDate()+6);\r\n    return [monday, sunday];\r\n  }\r\n  function computeWeeklyExpense(){\r\n    const [from, to] = getWeekRange(new Date());\r\n    const f = fmtDateInput(from), t = fmtDateInput(to);\r\n    let out=0;\r\n    for(const r of tx){\r\n      if(r.type==='out' && r.date>=f && r.date<=t) out+=r.amount;\r\n    }\r\n    return out;\r\n  }\r\n  function computeMonthlyExpense(){\r\n    const now = new Date();\r\n    const ym = now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0');\r\n    let out=0;\r\n    for(const r of tx){\r\n      if(r.type==='out' && r.date.startsWith(ym)) out+=r.amount;\r\n    }\r\n    return out;\r\n  }\r\n\r\n  \/\/ Rendering\r\n  function drawAll(){\r\n    \/\/ KPI\r\n    const bal = computeBalances();\r\n    const net = bal.cash + bal.biz + bal.personal;\r\n    els.kpiNet.textContent = formatVND(net);\r\n    const savM = computeCurrentMonthSavings();\r\n    const savLabel = els.kpiSavings;\r\n    const sText = t('thisMonthSavings') + ': ' + formatVND(savM);\r\n    savLabel.innerHTML = sText;\r\n    const deltas = recentDeltas();\r\n    els.kpiCash.textContent = formatVND(bal.cash);\r\n    els.kpiBiz.textContent = formatVND(bal.biz);\r\n    els.kpiPersonal.textContent = formatVND(bal.personal);\r\n    setDelta(els.kpiCashDelta, deltas.cash);\r\n    setDelta(els.kpiBizDelta, deltas.biz);\r\n    setDelta(els.kpiPersonalDelta, deltas.personal);\r\n\r\n    \/\/ Budgets\r\n    const wPlan = settings.budgets.weekly|0;\r\n    const mPlan = settings.budgets.monthly|0;\r\n    const wAct = computeWeeklyExpense();\r\n    const mAct = computeMonthlyExpense();\r\n    els.weeklyActual.textContent = formatVND(wAct);\r\n    els.monthlyActual.textContent = formatVND(mAct);\r\n    els.weeklyPlan.textContent = formatVND(wPlan);\r\n    els.monthlyPlan.textContent = formatVND(mPlan);\r\n    els.progWeekly.style.width = (wPlan? Math.min(100, (wAct\/wPlan)*100) : 0) + '%';\r\n    els.progMonthly.style.width = (mPlan? Math.min(100, (mAct\/mPlan)*100) : 0) + '%';\r\n\r\n    \/\/ Charts\r\n    drawIncomeExpense();\r\n    drawSavings();\r\n\r\n    \/\/ Table\r\n    renderTable();\r\n\r\n    \/\/ Integrity\r\n    updateIntegrityPill();\r\n  }\r\n\r\n  function setDelta(el, val){\r\n    if(val>0){ el.textContent = '+'+formatVND(val); el.className='delta good'; }\r\n    else if(val<0){ el.textContent = formatVND(val); el.className='delta bad'; }\r\n    else { el.textContent = ''; el.className='delta'; }\r\n  }\r\n  function recentDeltas(){\r\n    \/\/ last 7 days net delta by account\r\n    const since = new Date(); since.setDate(since.getDate()-7);\r\n    const s = fmtDateInput(since);\r\n    const d = {cash:0,biz:0,personal:0};\r\n    for(const r of tx){\r\n      if(r.date>=s){\r\n        const sgn = r.type==='in'?1:-1;\r\n        d[r.account]+= sgn*r.amount;\r\n      }\r\n    }\r\n    return d;\r\n  }\r\n\r\n  \/\/ Transactions table\r\n  function renderTable(){\r\n    const acct = els.fltAccount.value;\r\n    const typ = els.fltType.value;\r\n    const mon = els.fltMonth.value; \/\/ yyyy-mm\r\n    const kw = els.fltSearch.value.trim().toLowerCase();\r\n\r\n    const rows = tx.filter(r=>{\r\n      if(acct!=='all' && r.account!==acct) return false;\r\n      if(typ!=='all' && r.type!==typ) return false;\r\n      if(mon && !r.date.startsWith(mon)) return false;\r\n      if(kw){\r\n        const hay = [r.category, r.memo, r.account, r.type, r.amount].join(' ').toLowerCase();\r\n        if(!hay.includes(kw)) return false;\r\n      }\r\n      return true;\r\n    }).sort((a,b)=>{\r\n      if(a.date===b.date) return b.createdAt - a.createdAt;\r\n      return a.date>b.date? -1:1;\r\n    });\r\n\r\n    els.txTbody.innerHTML = '';\r\n    let sumIn=0, sumOut=0;\r\n    for(const r of rows){\r\n      const tr = document.createElement('tr');\r\n      const acctLabel = t(r.account);\r\n      const typeLabel = r.type==='in'? t('income') : t('expense');\r\n      const amtClass = r.type==='in'?'amt-pos':'amt-neg';\r\n      tr.innerHTML = `\r\n        <td>${r.date}<\/td>\r\n        <td>${acctLabel}<\/td>\r\n        <td>${typeLabel}<\/td>\r\n        <td>${escapeHtml(r.category||'')}<\/td>\r\n        <td>${escapeHtml(r.memo||'')}<\/td>\r\n        <td class=\"${amtClass}\">${formatVND(r.amount)}<\/td>\r\n        <td><button class=\"wl-btn\" data-id=\"${r.id}\" data-act=\"del\">${t('btnDelete')}<\/button><\/td>\r\n      `;\r\n      els.txTbody.appendChild(tr);\r\n      if(r.type==='in') sumIn+=r.amount; else sumOut+=r.amount;\r\n    }\r\n    els.txCount.textContent = rows.length;\r\n    els.sumIn.textContent = formatVND(sumIn);\r\n    els.sumOut.textContent = formatVND(sumOut);\r\n    els.sumSav.textContent = formatVND(sumIn - sumOut);\r\n\r\n    \/\/ Bind delete\r\n    $$('#txTbody [data-act=\"del\"]').forEach(btn=>{\r\n      btn.addEventListener('click', ()=>{\r\n        if(!confirm(t('confirmDelete'))) return;\r\n        const id = btn.getAttribute('data-id');\r\n        const idx = tx.findIndex(x=>x.id===id);\r\n        if(idx>=0){\r\n          tx.splice(idx,1);\r\n          saveTx();\r\n          renderTable();\r\n          drawAll();\r\n          showToast(t('deleted'));\r\n        }\r\n      });\r\n    });\r\n  }\r\n\r\n  \/\/ Charts (simple canvas without external libs)\r\n  function prepCanvas(cnv){\r\n    const dpr = Math.max(1, window.devicePixelRatio || 1);\r\n    const rect = cnv.getBoundingClientRect();\r\n    cnv.width = Math.floor(rect.width * dpr);\r\n    cnv.height = Math.floor(rect.height * dpr);\r\n    const ctx = cnv.getContext('2d');\r\n    ctx.scale(dpr, dpr);\r\n    return {ctx, w: rect.width, h: rect.height};\r\n  }\r\n  function drawIncomeExpense(){\r\n    const {ctx,w,h} = prepCanvas(els.chartIE);\r\n    ctx.clearRect(0,0,w,h);\r\n    const data = computeMonthlyIE();\r\n    const labels = data.map(d=> d.key.slice(2)); \/\/ 'yy-mm'\r\n    const inVals = data.map(d=> d.in);\r\n    const outVals = data.map(d=> d.out);\r\n    const pad = {l:40, r:10, t:20, b:30};\r\n    const cw = w - pad.l - pad.r;\r\n    const ch = h - pad.t - pad.b;\r\n    const maxVal = Math.max(1, ...inVals, ...outVals);\r\n    const yScale = ch \/ maxVal;\r\n    const colW = cw \/ data.length;\r\n    const barW = Math.max(6, (colW - 8)\/2);\r\n\r\n    \/\/ axes\r\n    ctx.strokeStyle = 'rgba(150,160,180,0.25)';\r\n    ctx.lineWidth = 1;\r\n    ctx.beginPath();\r\n    ctx.moveTo(pad.l, pad.t);\r\n    ctx.lineTo(pad.l, pad.t+ch);\r\n    ctx.lineTo(pad.l+cw, pad.t+ch);\r\n    ctx.stroke();\r\n\r\n    \/\/ grid lines\r\n    ctx.font = '12px system-ui';\r\n    ctx.fillStyle = 'var(--muted)';\r\n    ctx.textAlign = 'center';\r\n    \/\/ labels\r\n    labels.forEach((lb, i)=>{\r\n      if(i%2===0){\r\n        ctx.fillStyle = 'rgba(150,160,180,0.7)';\r\n        ctx.fillText(lb, pad.l + i*colW + colW\/2, pad.t+ch+18);\r\n      }\r\n    });\r\n\r\n    \/\/ bars\r\n    for(let i=0;i<data.length;i++){\r\n      const x = pad.l + i*colW + 4;\r\n      \/\/ income\r\n      const hIn = inVals[i]*yScale;\r\n      ctx.fillStyle = grad(ctx, x, pad.t+ch-hIn, barW, hIn, '#4e8cff', '#7d60ff');\r\n      ctx.fillRect(x, pad.t+ch - hIn, barW, hIn);\r\n      \/\/ expense\r\n      const hOut = outVals[i]*yScale;\r\n      ctx.fillStyle = grad(ctx, x+barW+4, pad.t+ch-hOut, barW, hOut, '#ef4444', '#f59e0b');\r\n      ctx.fillRect(x+barW+4, pad.t+ch - hOut, barW, hOut);\r\n    }\r\n\r\n    \/\/ legend\r\n    legend(ctx, w-140, 10, [\r\n      {c1:'#4e8cff', c2:'#7d60ff', label: t('income')},\r\n      {c1:'#ef4444', c2:'#f59e0b', label: t('expense')}\r\n    ]);\r\n  }\r\n  function drawSavings(){\r\n    const {ctx,w,h} = prepCanvas(els.chartSavings);\r\n    ctx.clearRect(0,0,w,h);\r\n    const data = computeMonthlyIE();\r\n    const labels = data.map(d=> d.key.slice(2));\r\n    const vals = data.map(d=> d.in - d.out);\r\n\r\n    const pad = {l:40, r:10, t:10, b:30};\r\n    const cw = w - pad.l - pad.r;\r\n    const ch = h - pad.t - pad.b;\r\n    const maxAbs = Math.max(1, ...vals.map(v=>Math.abs(v)));\r\n    const midY = pad.t + ch\/2;\r\n    const yScale = (ch\/2) \/ maxAbs;\r\n\r\n    \/\/ axis\r\n    ctx.strokeStyle = 'rgba(150,160,180,0.25)';\r\n    ctx.beginPath();\r\n    ctx.moveTo(pad.l, midY);\r\n    ctx.lineTo(pad.l+cw, midY);\r\n    ctx.stroke();\r\n\r\n    \/\/ line\r\n    ctx.lineWidth = 2;\r\n    ctx.beginPath();\r\n    for(let i=0;i<vals.length;i++){\r\n      const x = pad.l + (cw\/(vals.length-1)) * i;\r\n      const y = midY - vals[i]*yScale;\r\n      if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);\r\n    }\r\n    const gr = ctx.createLinearGradient(0,0,w,0);\r\n    gr.addColorStop(0, '#22c55e');\r\n    gr.addColorStop(1, '#4e8cff');\r\n    ctx.strokeStyle = gr;\r\n    ctx.stroke();\r\n\r\n    \/\/ fill under\r\n    const path = new Path2D();\r\n    for(let i=0;i<vals.length;i++){\r\n      const x = pad.l + (cw\/(vals.length-1)) * i;\r\n      const y = midY - vals[i]*yScale;\r\n      if(i===0) path.moveTo(x,y); else path.lineTo(x,y);\r\n    }\r\n    path.lineTo(pad.l+cw, midY);\r\n    path.lineTo(pad.l, midY);\r\n    ctx.fillStyle = 'rgba(78,140,255,0.15)';\r\n    ctx.fill(path);\r\n\r\n    \/\/ labels\r\n    ctx.font = '12px system-ui';\r\n    ctx.fillStyle = 'rgba(150,160,180,0.7)';\r\n    ctx.textAlign = 'center';\r\n    labels.forEach((lb, i)=>{\r\n      if(i%2===0){\r\n        const x = pad.l + (cw\/(labels.length-1))*i;\r\n        ctx.fillText(lb, x, h-10);\r\n      }\r\n    });\r\n  }\r\n  function grad(ctx, x,y,w,h,c1,c2){\r\n    const g = ctx.createLinearGradient(x,y,x+w,y+h);\r\n    g.addColorStop(0,c1); g.addColorStop(1,c2);\r\n    return g;\r\n  }\r\n  function legend(ctx, x, y, items){\r\n    ctx.font = '12px system-ui';\r\n    items.forEach((it,i)=>{\r\n      ctx.fillStyle = grad(ctx,x,y+ i*18, 20, 12, it.c1,it.c2);\r\n      ctx.fillRect(x, y+i*18, 16, 10);\r\n      ctx.fillStyle='rgba(200,210,225,0.9)';\r\n      ctx.fillText(it.label, x+22, y+9+i*18);\r\n    });\r\n  }\r\n\r\n  \/\/ Integrity\r\n  function updateIntegrityPill(){\r\n    \/\/ verify integer amounts and balance cross-check\r\n    let ok = true;\r\n    for(const r of tx){\r\n      if(!Number.isFinite(r.amount) || r.amount!==Math.round(r.amount) || r.amount<0){ ok=false; break; }\r\n      if(!r.date || typeof r.type!=='string' || typeof r.account!=='string'){ ok=false; break; }\r\n    }\r\n    \/\/ recompute vs manual sum to double-check\r\n    const bal = computeBalances();\r\n    let check = {cash: settings.initBalances.cash, biz: settings.initBalances.biz, personal: settings.initBalances.personal};\r\n    for(const r of tx){\r\n      const sgn = r.type==='in'?1:-1;\r\n      check[r.account]+= sgn*r.amount;\r\n    }\r\n    if(check.cash!==bal.cash || check.biz!==bal.biz || check.personal!==bal.personal) ok=false;\r\n\r\n    els.integrityPill.innerHTML = (ok\r\n      ? `<span class=\"ok-dot\"><\/span>${t('integrityOk')}`\r\n      : `<span class=\"warn-dot\"><\/span>${t('integrityWarn')}`\r\n    );\r\n  }\r\n\r\n  \/\/ Export\r\n  function exportCSV(){\r\n    const rows = [\r\n      ['id','date','type','account','category','memo','amount(VND)'],\r\n      ...tx.map(r=>[r.id, r.date, r.type, r.account, escCsv(r.category), escCsv(r.memo), String(r.amount)])\r\n    ];\r\n    const csv = '\\uFEFF' + rows.map(r=>r.map(csvCell).join(',')).join('\\n');\r\n    downloadBlob(csv, `ledger_vnd_${todayStr()}.csv`, 'text\/csv;charset=utf-8;');\r\n  }\r\n  function exportExcel(){\r\n    \/\/ Create simple HTML table which Excel opens as .xls\r\n    const th = ['id','date','type','account','category','memo','amount(VND)'];\r\n    const trh = `<tr>${th.map(h=>`<th>${escapeHtml(h)}<\/th>`).join('')}<\/tr>`;\r\n    const trs = tx.map(r=>`<tr>\r\n      <td>${escapeHtml(r.id)}<\/td>\r\n      <td>${r.date}<\/td>\r\n      <td>${r.type}<\/td>\r\n      <td>${r.account}<\/td>\r\n      <td>${escapeHtml(r.category||'')}<\/td>\r\n      <td>${escapeHtml(r.memo||'')}<\/td>\r\n      <td style=\"mso-number-format:'0';\">${r.amount}<\/td>\r\n    <\/tr>`).join('');\r\n    const html = `\r\n      <html xmlns:o=\"urn:schemas-microsoft-com:office:office\"\r\n            xmlns:x=\"urn:schemas-microsoft-com:office:excel\"\r\n            xmlns=\"http:\/\/www.w3.org\/TR\/REC-html40\">\r\n      <head><meta charset=\"utf-8\"><\/head>\r\n      <body>\r\n        <table border=\"1\">${trh}${trs}<\/table>\r\n      <\/body><\/html>`;\r\n    const blob = new Blob([html], {type:'application\/vnd.ms-excel'});\r\n    downloadBlob(blob, `ledger_vnd_${todayStr()}.xls`, 'application\/vnd.ms-excel');\r\n  }\r\n  function backupJSON(){\r\n    const payload = {\r\n      settings, tx\r\n    };\r\n    const json = '\\uFEFF'+JSON.stringify(payload);\r\n    downloadBlob(json, `ledger_vnd_backup_${todayStr()}.json`, 'application\/json;charset=utf-8;');\r\n  }\r\n  function restoreJSON(e){\r\n    const f = e.target.files[0];\r\n    if(!f) return;\r\n    const reader = new FileReader();\r\n    reader.onload = function(){\r\n      try{\r\n        const obj = JSON.parse(reader.result);\r\n        if(!obj || !Array.isArray(obj.tx) || !obj.settings) throw new Error('bad file');\r\n        \/\/ minimal validation\r\n        settings = {\r\n          ...defaultSettings(),\r\n          ...obj.settings,\r\n          budgets: {\r\n            weekly: toInt(obj.settings?.budgets?.weekly||0),\r\n            monthly: toInt(obj.settings?.budgets?.monthly||0)\r\n          },\r\n          initBalances:{\r\n            cash: toInt(obj.settings?.initBalances?.cash||0),\r\n            biz: toInt(obj.settings?.initBalances?.biz||0),\r\n            personal: toInt(obj.settings?.initBalances?.personal||0),\r\n          }\r\n        };\r\n        tx = obj.tx.map(n=>({\r\n          id: n.id || uid(),\r\n          type: n.type==='in'?'in':'out',\r\n          account: ['cash','biz','personal'].includes(n.account)?n.account:'cash',\r\n          amount: toInt(n.amount||0),\r\n          date: n.date || fmtDateInput(new Date()),\r\n          category: n.category||'',\r\n          memo: n.memo||'',\r\n          createdAt: n.createdAt || Date.now()\r\n        }));\r\n        saveSettings(); saveTx();\r\n        \/\/ Refill inputs\r\n        els.langSelect.value = settings.lang;\r\n        applyI18N();\r\n        applyTheme(settings.theme);\r\n        setAmtInput(els.inpWeeklyPlan, settings.budgets.weekly);\r\n        setAmtInput(els.inpMonthlyPlan, settings.budgets.monthly);\r\n        setAmtInput(els.initCash, settings.initBalances.cash);\r\n        setAmtInput(els.initBiz, settings.initBalances.biz);\r\n        setAmtInput(els.initPersonal, settings.initBalances.personal);\r\n        drawAll();\r\n        showToast(t('imported'));\r\n        els.restoreFile.value = '';\r\n      }catch(err){\r\n        alert('Invalid backup file.');\r\n      }\r\n    };\r\n    reader.readAsText(f);\r\n  }\r\n\r\n  \/\/ Helpers\r\n  function escCsv(s){\r\n    return (s||'').replace(\/[\\r\\n]+\/g,' ');\r\n  }\r\n  function csvCell(s){\r\n    if(s==null) return '';\r\n    const str = String(s);\r\n    if(\/[\",\\n]\/.test(str)) return `\"${str.replace(\/\"\/g,'\"\"')}\"`;\r\n    return str;\r\n  }\r\n  function downloadBlob(content, filename, mime){\r\n    let blob = content instanceof Blob ? content : new Blob([content], {type:mime});\r\n    const url = URL.createObjectURL(blob);\r\n    const a = document.createElement('a');\r\n    a.href = url; a.download = filename;\r\n    document.body.appendChild(a);\r\n    a.click();\r\n    setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); }, 100);\r\n  }\r\n  function todayStr(){\r\n    const d = new Date();\r\n    return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`;\r\n  }\r\n  function uid(){\r\n    return 'id_'+Math.random().toString(36).slice(2,10)+Date.now().toString(36);\r\n  }\r\n  function escapeHtml(s){\r\n    return (s||'').replace(\/[&<>\"']\/g, c=>({ '&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#039;'}[c]));\r\n  }\r\n  function debounce(fn,ms){\r\n    let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a),ms); };\r\n  }\r\n  function alertI18n(key){ alert(t(key)); }\r\n\r\n  \/\/ -------------- Lightweight self-test (doesn't change user data) --------------\r\n  \/\/ Ensures integrity math and formatter work. Runs on isolated data.\r\n  (function selfTest(){\r\n    try{\r\n      \/\/ pure compute with synthetic tx\r\n      const init = {cash: 1000, biz: 0, personal: 0};\r\n      const tempTx = [\r\n        {type:'in', account:'cash', amount: 2000, date:'2025-01-10'},\r\n        {type:'out', account:'cash', amount: 500, date:'2025-01-11'},\r\n        {type:'in', account:'biz', amount: 300, date:'2025-01-12'},\r\n      ];\r\n      \/\/ compute\r\n      let c = {...init};\r\n      for(const r of tempTx){ c[r.account] += (r.type==='in'?1:-1)*r.amount; }\r\n      if(c.cash!==2500 || c.biz!==300 || c.personal!==0) throw new Error('balance mismatch');\r\n      \/\/ parse\/format\r\n      if(parseAmt('1,234,567 VND')!==1234567) throw new Error('parseAmt failed');\r\n      formatVND(123); \/\/ should not throw\r\n      \/\/ ok\r\n    }catch(e){\r\n      console.warn('Self-test failed', e);\r\n    }\r\n  })();\r\n\r\n})();\r\n<\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>","protected":false},"excerpt":{"rendered":"<p>\uac00\uacc4\ubd80 \u00b7 Wealth Ledger (VND) Wealth Ledger VND \uc2e4\uc2dc\uac04 \uac00\uacc4\ubd80 \ud55c\uad6d\uc5b4Ti\u1ebfng Vi\u1ec7t \ud83c\udf19 Excel CSV Backup Import \ube60\ub978 \uc785\ub825 \ub370\uc774\ud130 \ubb34\uacb0\uc131 \uc815\uc0c1 \uc218\uc785 \uc9c0\ucd9c \ud604\uae08\ubc95\uc778\uacc4\uc88c\uac1c\uc778\uacc4\uc88c VND \ucd94\uac00 \ucd1d\uc790\uc0b0 \u20ab0 \uc774\ubc88\ub2ec \uc800\ucd95: \u20ab0 \ud604\uae08 \u20ab0 \ubc95\uc778\uacc4\uc88c \u20ab0 \uac1c\uc778\uacc4\uc88c \u20ab0 \uc9c0\ucd9c \uacc4\ud68d \uc8fc\uac04 \uc9c0\ucd9c \uacc4\ud68d \u20ab0 \/ \u20ab0 \uc6d4\uac04 \uc9c0\ucd9c \uacc4\ud68d \u20ab0 \/ \u20ab0 VND VND \uc800\uc7a5 [&hellip;]<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"wf_page_folders":[],"class_list":["post-3595","page","type-page","status-publish","format-standard","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/pages\/3595","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/comments?post=3595"}],"version-history":[{"count":0,"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/pages\/3595\/revisions"}],"wp:attachment":[{"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/media?parent=3595"}],"wp:term":[{"taxonomy":"wf_page_folders","embeddable":true,"href":"https:\/\/hcbiopharm.net\/en\/wp-json\/wp\/v2\/wf_page_folders?post=3595"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}