Bash allowlist pattern 'Bash(curl *)' in settings.local.json not matching — repeated permission prompts
Status Open
Maintainer reply None cached
Activity 6 comments · opened Feb 28, 2026
Bug Description
Bash(curl *) is listed in permissions.allow in .claude/settings.local.json, but Claude Code still prompts for permission on every curl invocation. During a single session with ~20 curl commands (API endpoint testing), the user had to approve "Yes, and don't ask again for: curl:*" repeatedly.
Reproduction
- Add both patterns to
.claude/settings.local.json:
``json``
{
"permissions": {
"allow": [
"Bash(curl *)",
"Bash(curl:*)"
]
}
}
- Ask Claude Code to test API endpoints using curl (e.g. "test these 12 endpoints with curl")
- Expected: curl commands execute without permission prompts
Actual: Each curl command triggers a permission prompt: "Yes, and don't ask again for: curl:*"
Additional Context
- The "don't ask again" approvals during the session don't appear to persist either — no project-level
settings.jsonwas created at~/.claude/projects/<project>/settings.jsonafter the session - Other
Bash(...)patterns in the same file (e.g.Bash(node *),Bash(git *)) appear to work correctly - It's possible the colon in
curl:*vs space incurl *is causing a format mismatch between what the allowlist expects and what the permission check evaluates
Environment
- Claude Code CLI (Opus 4.6)
- Linux (LXC container)
- Bash shell
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
Can confirm this is still happening as of 2.1.71 on mac os
Still happening on 2.17.1 Windows
Similar problem: https://github.com/anthropics/claude-code/issues/34999
I still see this with 2.1.114 on win11 for "mkdir"
<!DOCTYPE html>
<html lang="bn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>কড়াগণ্ডা – স্মার্ট লেজার বুক</title>
<style>
:root{
--page-bg:#0b2444;
--paper:#ffffff;
--paper-shadow:#eaf1f9;
--card-bg:#f3f7fb;
--margin-red:#b2433a;
--rule-blue:#2f6690;
--ink:#16324f;
--ink-soft:#4c6a86;
--gold:#2f6690;
--green:#123a5e;
--green-soft:#2f6690;
--income:#1f6b6b;
--danger:#a83232;
--border:#c7d9ea;
--line-h:38px;
}
html[data-theme="dark"]{
--page-bg:#0f1922;
--paper:#0f1922;
--paper-shadow:#16222d;
--card-bg:#1b2a37;
--margin-red:#d8776c;
--rule-blue:#6fa8d8;
--ink:#dce8f2;
--ink-soft:#8fadc7;
--gold:#5b9bd5;
--green:#5b9bd5;
--green-soft:#4a86b8;
--income:#6fcaca;
--danger:#e2837a;
--border:#28394a;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
body{
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
color:var(--ink);
background:
radial-gradient(ellipse at top left, rgba(255,255,255,0.06), transparent 55%),
var(--page-bg);
min-height:100vh;
padding:24px 12px 60px;
transition:background .25s ease, color .25s ease;
}
.wrap{max-width:800px;margin:0 auto;}
/ Header /
.masthead{
display:flex;justify-content:space-between;align-items:flex-end;
padding:0 6px 14px;
border-bottom:3px double rgba(255,255,255,0.35);
margin-bottom:6px;
gap:10px;flex-wrap:wrap;
}
.masthead h1{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;
font-weight:700;
font-size:32px;
margin:0;
color:#ffffff;
letter-spacing:0.5px;
}
.masthead .sub{
font-size:13px;color:#a9c6e0;margin-top:4px;
}
.head-right{display:flex;align-items:center;gap:8px;}
.today-stamp{
font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;
font-size:12px;
color:#bcd7ec;
border:1.5px solid #bcd7ec;
border-radius:3px;
padding:5px 10px;
transform:rotate(-3deg);
white-space:nowrap;
}
.theme-btn{
width:38px;height:38px;border-radius:50%;
border:1.5px solid rgba(255,255,255,0.4);
background:#ffffff;
color:var(--ink);
cursor:pointer;font-size:16px;
display:flex;align-items:center;justify-content:center;
-webkit-tap-highlight-color:transparent;
}
.theme-btn:hover{border-color:var(--green-soft);}
.theme-btn:focus-visible{outline:2px solid var(--gold);outline-offset:2px;}
/ Summary strip /
.summary{
display:grid;
grid-template-columns:repeat(3,1fr);
gap:10px;
margin:20px 0 16px;
}
.sum-card{
background:var(--paper-shadow);
border-radius:6px;
padding:12px 14px;
position:relative;
overflow:hidden;
}
.sum-card::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--green);
}
.sum-card.income::before{background:var(--income);}
.sum-card.expense::before{background:var(--margin-red);}
.sum-card.balance::before{background:var(--gold);}
.sum-card .lbl{font-size:11.5px;color:var(--ink-soft);margin-bottom:4px;}
.sum-card .val{
font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;
font-size:19px;font-weight:700;color:var(--ink);
}
.sum-card.balance .val.neg{color:var(--danger);}
/ Budget bar /
.budget-box{
background:var(--paper-shadow);border-radius:6px;padding:14px 16px;margin-bottom:20px;
}
.budget-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px;flex-wrap:wrap;}
.budget-head h3{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;font-weight:500;font-size:14.5px;
margin:0;color:var(--ink-soft);
}
.budget-edit-btn{
font-size:11.5px;color:var(--green-soft);background:none;border:1px solid var(--border);
border-radius:4px;padding:5px 9px;cursor:pointer;
}
.budget-bar-bg{background:rgba(0,0,0,0.08);border-radius:4px;height:16px;overflow:hidden;}
html[data-theme="dark"] .budget-bar-bg{background:rgba(255,255,255,0.08);}
.budget-bar-fill{height:100%;background:var(--green-soft);transition:width .4s ease;}
.budget-bar-fill.warn{background:var(--gold);}
.budget-bar-fill.over{background:var(--danger);}
.budget-text{font-size:12px;color:var(--ink-soft);margin-top:6px;font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;}
.budget-alert{color:var(--danger);font-weight:600;margin-top:4px;font-size:12.5px;font-family:'Hind Siliguri',sans-serif;}
.budget-setup{display:flex;gap:8px;margin-top:8px;}
.budget-setup input{flex:1;}
.budget-setup button{
background:var(--green);color:#f3ede0;border:none;border-radius:4px;padding:0 16px;cursor:pointer;font-weight:600;
}
/ Charts /
.charts-section{margin-bottom:24px;}
.charts-section h3{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;
font-size:15px;color:#bcd7ec;margin:0 0 10px;font-weight:500;
}
.charts-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
@media (max-width:600px){.charts-grid{grid-template-columns:1fr;}}
.chart-card{background:var(--paper-shadow);border-radius:6px;padding:14px;}
.chart-card .chart-title{font-size:12px;color:var(--ink-soft);margin-bottom:8px;}
.pie-wrap{display:flex;align-items:center;gap:14px;flex-wrap:wrap;}
.pie-legend{flex:1;min-width:120px;font-size:11.5px;}
.legend-row{display:flex;align-items:center;gap:6px;margin-bottom:5px;}
.legend-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0;}
.legend-name{flex:1;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.legend-val{font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;color:var(--ink-soft);}
.no-data{font-size:12.5px;color:var(--ink-soft);text-align:center;padding:20px 0;}
/ Category bars (legacy list under charts) /
.cat-section{margin-bottom:24px;}
.cat-section h3{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;
font-size:15px;color:#bcd7ec;margin:0 0 10px;font-weight:500;
}
.cat-row{
display:flex;align-items:center;gap:10px;margin-bottom:7px;font-size:13px;
}
.cat-name{width:100px;flex-shrink:0;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.cat-bar-bg{
flex:1;background:rgba(0,0,0,0.06);border-radius:3px;height:14px;overflow:hidden;
}
html[data-theme="dark"] .cat-bar-bg{background:rgba(255,255,255,0.07);}
.cat-bar-fill{
height:100%;background:var(--green-soft);border-radius:3px;
transition:width .4s ease;
}
.cat-amt{
font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;width:78px;text-align:right;flex-shrink:0;color:var(--ink-soft);
}
/ Category management /
.catmgmt-box{
background:var(--paper);border:1.5px solid var(--border);border-radius:6px;padding:14px 16px;margin-bottom:20px;
}
.catmgmt-head{display:flex;justify-content:space-between;align-items:center;cursor:pointer;}
.catmgmt-head h3{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;font-weight:500;font-size:14.5px;
margin:0;color:var(--green);
}
.catmgmt-toggle{background:none;border:none;color:var(--ink-soft);font-size:14px;cursor:pointer;padding:6px;}
.catmgmt-body{margin-top:12px;display:none;}
.catmgmt-body.open{display:block;}
.catmgmt-tabs{display:flex;gap:8px;margin-bottom:10px;}
.catmgmt-tab{
flex:1;text-align:center;padding:8px;border-radius:4px;border:1px solid var(--border);
background:var(--card-bg);color:var(--ink-soft);font-size:12.5px;cursor:pointer;
}
.catmgmt-tab.active{background:var(--green);color:#f3ede0;border-color:var(--green);}
.catmgmt-list{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:10px;}
.catmgmt-chip{
display:flex;align-items:center;gap:6px;background:rgba(31,75,63,0.09);
padding:5px 6px 5px 11px;border-radius:14px;font-size:12.5px;
}
.catmgmt-chip button{
background:none;border:none;color:var(--ink-soft);cursor:pointer;font-size:13px;
width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;
}
.catmgmt-chip button:hover{color:var(--danger);}
.catmgmt-add{display:flex;gap:8px;}
.catmgmt-add input{flex:1;}
.catmgmt-add button{
background:var(--green-soft);color:#f3ede0;border:none;border-radius:4px;padding:0 14px;cursor:pointer;font-weight:600;font-size:13px;
}
/ Combined entry + dues section /
.combo-box{
background:var(--paper);border:1.5px solid var(--ink-soft);border-radius:6px;padding:16px;margin-bottom:26px;
}
.combo-head h3{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;font-weight:500;font-size:16px;
margin:0 0 12px;color:var(--green);
}
.combo-tabs{display:flex;gap:8px;margin-bottom:16px;}
.combo-tab{
flex:1;text-align:center;padding:10px;border-radius:5px;border:1.5px solid var(--border);
background:var(--card-bg);color:var(--ink-soft);font-weight:600;font-size:13.5px;cursor:pointer;
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
}
.combo-tab.active{background:var(--green);border-color:var(--green);color:#f3ede0;}
.combo-panel{display:none;}
.combo-panel.active{display:block;}
.combo-panel > .entry-box,
.combo-panel > .dues-box{
background:none;border:none;padding:0;margin-bottom:0;
}
/ Dues (বাকি হিসাব) section /
.dues-box{
background:var(--paper);border:1.5px solid var(--ink-soft);border-radius:6px;padding:16px;margin-bottom:26px;
}
.dues-box h3{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;font-weight:500;font-size:15px;
margin:0 0 4px;color:var(--green);
}
.dues-box .dues-sub{font-size:11.5px;color:var(--ink-soft);margin-bottom:12px;}
.dues-mini{display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;}
.dues-mini .mini-chip{
flex:1;min-width:130px;background:var(--paper-shadow);border-radius:6px;padding:10px 12px;position:relative;overflow:hidden;
}
.dues-mini .mini-chip::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px;}
.dues-mini .mini-chip.receivable::before{background:var(--income);}
.dues-mini .mini-chip.payable::before{background:var(--margin-red);}
.dues-mini .mini-chip .lbl{font-size:11px;color:var(--ink-soft);margin-bottom:3px;}
.dues-mini .mini-chip .val{font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;font-size:17px;font-weight:700;}
.dues-mini .mini-chip.receivable .val{color:var(--income);}
.dues-mini .mini-chip.payable .val{color:var(--margin-red);}
.due-type-toggle{display:flex;gap:8px;margin-bottom:12px;}
.due-type-toggle button{
flex:1;padding:10px;border-radius:5px;border:1.5px solid var(--border);
background:var(--card-bg);color:var(--ink-soft);font-weight:600;font-size:13.5px;cursor:pointer;
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
}
.due-type-toggle button.active.receivable{background:var(--income);border-color:var(--income);color:#fff;}
.due-type-toggle button.active.payable{background:var(--margin-red);border-color:var(--margin-red);color:#fff;}
.due-form-grid{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;}
.due-form-grid .due-name{flex:1 1 160px;}
.due-form-grid .due-amount{flex:1 1 110px;}
.due-form-grid .due-date{flex:1 1 130px;}
.due-form-grid .due-reminder{flex:1 1 160px;}
.due-form-grid .due-note{flex:1 1 100%;}
/ Reminder banner /
.reminder-banner{
background:#fff3e0;border:1.5px solid var(--danger);border-radius:6px;
padding:12px 14px;margin-bottom:16px;
}
html[data-theme="dark"] .reminder-banner{background:#3a2418;}
.reminder-banner .rb-head{
display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px;
}
.reminder-banner .rb-title{font-weight:700;font-size:13.5px;color:var(--danger);}
.reminder-banner .rb-close{
background:none;border:none;color:var(--ink-soft);cursor:pointer;font-size:16px;
width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;
}
.reminder-banner .rb-close:hover{color:var(--danger);}
.reminder-banner .rb-item{
display:flex;justify-content:space-between;align-items:center;gap:8px;
font-size:12.5px;padding:6px 0;border-top:1px dashed var(--border);
}
.reminder-banner .rb-item:first-of-type{border-top:none;}
.reminder-banner .rb-item .rb-name{font-weight:600;color:var(--ink);}
.reminder-banner .rb-item .rb-due{color:var(--ink-soft);font-size:11.5px;}
.reminder-banner .rb-item .rb-amt{
font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;font-weight:700;color:var(--danger);flex-shrink:0;
}
.due-item .due-reminder-tag{
font-size:10.5px;color:var(--danger);flex-shrink:0;
}
/ Search / filter bars /
.filter-bar{
display:flex;flex-wrap:wrap;gap:7px;margin-bottom:14px;padding:10px;
background:var(--card-bg);border:1px solid var(--border);border-radius:6px;
}
.filter-bar input, .filter-bar select{
padding:8px 9px;min-height:38px;font-size:12.5px;
}
.filter-bar .f-search{flex:1 1 150px;}
.filter-bar .f-type{flex:1 1 90px;}
.filter-bar .f-category{flex:1 1 110px;}
.filter-bar .f-date{flex:1 1 118px;}
.filter-bar .f-clear{
flex-shrink:0;border:1px solid var(--border);background:var(--paper);color:var(--ink-soft);
border-radius:4px;padding:0 12px;min-height:38px;cursor:pointer;font-size:12px;
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
}
.filter-bar .f-clear:hover{border-color:var(--danger);color:var(--danger);}
.filter-empty{text-align:center;padding:22px 10px;color:var(--ink-soft);font-size:12.5px;}
.due-list{display:flex;flex-direction:column;gap:10px;}
.person-card{
background:var(--paper-shadow);border-radius:6px;padding:12px 14px;
}
.person-head{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap;}
.person-head .p-name{font-weight:700;font-size:14.5px;color:var(--ink);}
.person-head .p-net{
font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;font-size:12.5px;font-weight:700;
padding:3px 10px;border-radius:12px;
}
.person-head .p-net.pos{background:rgba(31,107,75,0.15);color:var(--income);}
.person-head .p-net.neg{background:rgba(168,50,50,0.15);color:var(--danger);}
.person-head .p-net.zero{background:rgba(0,0,0,0.06);color:var(--ink-soft);}
.due-item{
background:var(--card-bg);border:1px solid var(--border);border-radius:5px;
padding:9px 10px;margin-bottom:7px;
}
.due-item .due-row1{display:flex;align-items:center;gap:7px;flex-wrap:wrap;}
.due-item .due-type-tag{
font-size:10.5px;padding:1px 8px;border-radius:9px;flex-shrink:0;font-weight:600;
}
.due-item .due-type-tag.receivable{background:rgba(31,107,75,0.15);color:var(--income);}
.due-item .due-type-tag.payable{background:rgba(168,50,50,0.15);color:var(--danger);}
.due-item .due-date{font-size:11px;color:var(--ink-soft);flex-shrink:0;}
.due-item .due-note{flex:1;min-width:80px;font-size:12.5px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.due-item .due-amt-block{text-align:right;font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;font-size:12px;flex-shrink:0;}
.due-item .due-amt-block .orig{color:var(--ink-soft);}
.due-item .due-amt-block .remain{font-weight:700;color:var(--ink);}
.due-item .status-badge{
font-size:10px;padding:2px 7px;border-radius:8px;flex-shrink:0;font-weight:600;
}
.status-badge.pending{background:rgba(168,50,50,0.15);color:var(--danger);}
.status-badge.partial{background:rgba(169,127,46,0.18);color:var(--gold);}
.status-badge.paid{background:rgba(31,107,75,0.15);color:var(--income);}
.due-item .due-actions{display:flex;gap:4px;margin-top:6px;justify-content:flex-end;flex-wrap:wrap;}
.due-item .due-actions button{
font-size:11px;padding:5px 9px;border-radius:4px;border:1px solid var(--border);
background:var(--paper);color:var(--ink-soft);cursor:pointer;
}
.due-item .due-actions button:hover{border-color:var(--green-soft);color:var(--green-soft);}
.due-item .due-actions button.pay-btn:hover{border-color:var(--income);color:var(--income);}
.due-item .due-actions button.due-del:hover{border-color:var(--danger);color:var(--danger);}
.payment-history{margin-top:6px;padding-top:6px;border-top:1px dashed var(--border);}
.payment-history .ph-row{
display:flex;justify-content:space-between;font-size:11px;color:var(--ink-soft);padding:2px 0;
}
.payment-history .ph-row button{
background:none;border:none;color:var(--ink-soft);cursor:pointer;font-size:11px;padding:0 4px;
}
.payment-history .ph-row button:hover{color:var(--danger);}
.payment-form, .due-edit-form{
display:flex;gap:6px;flex-wrap:wrap;margin-top:7px;padding-top:7px;border-top:1px dashed var(--border);
}
.payment-form input{flex:1 1 100px;padding:8px;min-height:36px;font-size:13px;}
.due-edit-form input, .due-edit-form select{flex:1 1 100px;padding:8px;min-height:36px;font-size:13px;}
.payment-form .pf-actions, .due-edit-form .pf-actions{display:flex;gap:5px;flex-shrink:0;}
.payment-form button, .due-edit-form button{
border:none;border-radius:4px;padding:0 10px;cursor:pointer;font-size:12px;font-weight:600;min-height:36px;
}
.pf-save{background:var(--green-soft);color:#fff;}
.pf-cancel{background:transparent;border:1px solid var(--border) !important;color:var(--ink-soft);}
.dues-empty{text-align:center;padding:24px 10px;color:var(--ink-soft);font-size:13px;}
/ Entry form - styled like a ledger row you fill in /
.entry-box{
background:var(--paper);
border:1.5px solid var(--ink-soft);
border-radius:6px;
padding:16px;
margin-bottom:26px;
}
.entry-box h3{
font-family:'Noto Serif Bengali','Siyam Rupali','Kohinoor Bangla',serif;font-weight:500;font-size:15px;
margin:0 0 12px;color:var(--green);
}
label{display:block;font-size:11.5px;color:var(--ink-soft);margin-bottom:4px;}
.type-toggle{display:flex;gap:8px;margin-bottom:14px;}
.type-toggle button{
flex:1;padding:10px;border-radius:5px;border:1.5px solid var(--border);
background:var(--card-bg);color:var(--ink-soft);font-weight:600;font-size:13.5px;cursor:pointer;
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
}
.type-toggle button.active.expense{background:var(--margin-red);border-color:var(--margin-red);color:#fff;}
.type-toggle button.active.income{background:var(--income);border-color:var(--income);color:#fff;}
.date-row{margin-bottom:14px;}
.date-row label{margin-bottom:5px;}
#lineItems{display:flex;flex-direction:column;gap:8px;margin-bottom:10px;}
.line-item{
display:flex;gap:8px;align-items:flex-start;
background:rgba(31,75,63,0.04);
border:1px solid var(--border);
border-radius:5px;
padding:9px;
}
.line-item .li-amount{width:34%;flex-shrink:0;}
.line-item .li-category{width:34%;flex-shrink:0;}
.line-item .li-note{flex:1;min-width:0;}
.li-remove{
flex-shrink:0;
width:36px;height:44px;
border:1px solid var(--border);
background:var(--card-bg);
border-radius:4px;
color:var(--ink-soft);
cursor:pointer;
font-size:15px;
display:flex;align-items:center;justify-content:center;
-webkit-tap-highlight-color:transparent;
}
.li-remove:hover{border-color:var(--danger);color:var(--danger);}
.li-remove:disabled{opacity:0.3;cursor:default;}
.add-line-btn{
width:100%;
background:transparent;
border:1.5px dashed var(--gold);
color:var(--green-soft);
padding:10px;
min-height:44px;
border-radius:5px;
font-size:13.5px;
font-weight:600;
cursor:pointer;
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
margin-bottom:10px;
-webkit-tap-highlight-color:transparent;
}
.add-line-btn:hover{border-color:var(--green-soft);background:rgba(31,75,63,0.05);}
input, select{
width:100%;
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
font-size:16px;
padding:11px 10px;
border:1px solid var(--border);
border-radius:4px;
background:var(--card-bg);
color:var(--ink);
min-height:44px;
}
input:focus, select:focus{outline:2px solid var(--green-soft);outline-offset:1px;}
input[type=number]{font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;}
select{-webkit-appearance:none;appearance:none;background-image:linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%), linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);background-position:calc(100% - 18px) center, calc(100% - 13px) center;background-size:5px 5px, 5px 5px;background-repeat:no-repeat;padding-right:30px;}
.add-btn{
margin-top:12px;
width:100%;
background:var(--green);
color:#f3ede0;
border:none;
padding:13px;
min-height:46px;
border-radius:5px;
font-size:15px;
font-weight:600;
cursor:pointer;
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
letter-spacing:0.3px;
-webkit-tap-highlight-color:transparent;
}
.add-btn:hover{background:var(--green-soft);}
.add-btn:focus-visible{outline:2px solid var(--gold);outline-offset:2px;}
/ Ledger book of entries /
.ledger{
position:relative;
background:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent calc(var(--line-h) - 1px),
rgba(118,136,163,0.35) calc(var(--line-h) - 1px),
rgba(118,136,163,0.35) var(--line-h)
);
border:1px solid var(--border);
border-radius:4px;
padding:8px 14px 8px 54px;
}
.ledger::before{
content:'';
position:absolute;
left:40px;top:0;bottom:0;width:1.5px;
background:var(--margin-red);
}
.day-group{margin-bottom:2px;}
.day-label{
font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;
font-size:11px;color:var(--gold);
line-height:var(--line-h);
letter-spacing:0.5px;
}
.entry-row{
display:flex;align-items:center;gap:6px;
line-height:var(--line-h);
min-height:var(--line-h);
font-size:13.5px;
border-bottom:0;
}
.entry-row .note{
flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);
min-width:0;
}
.entry-row .cat-tag{
font-size:10.5px;color:var(--ink-soft);
background:rgba(31,75,63,0.09);
padding:1px 7px;border-radius:9px;flex-shrink:0;
max-width:76px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.entry-row .amt{
font-family:'JetBrains Mono','SFMono-Regular',Consolas,monospace;font-weight:600;
width:84px;text-align:right;flex-shrink:0;
font-size:13px;
}
.entry-row .amt.expense{color:var(--margin-red);}
.entry-row .amt.income{color:var(--income);}
.entry-row .edit,
.entry-row .del{
background:none;border:none;color:var(--ink-soft);cursor:pointer;font-size:15px;
flex-shrink:0;width:30px;height:30px;line-height:1;opacity:0.55;
display:flex;align-items:center;justify-content:center;
-webkit-tap-highlight-color:transparent;
}
.entry-row .edit:hover{opacity:1;color:var(--green-soft);}
.entry-row .del:hover{opacity:1;color:var(--danger);}
.entry-row .edit:focus-visible,
.entry-row .del:focus-visible{outline:1.5px solid var(--gold);opacity:1;}
.edit-row{
background:var(--card-bg);border:1px solid var(--gold);border-radius:5px;
padding:9px;margin:4px 0;display:flex;flex-wrap:wrap;gap:6px;
}
.edit-row input,.edit-row select{padding:8px;min-height:38px;font-size:13.5px;}
.edit-row .er-amount{width:90px;}
.edit-row .er-category{width:120px;}
.edit-row .er-date{width:130px;}
.edit-row .er-note{flex:1;min-width:100px;}
.edit-row .er-actions{display:flex;gap:5px;}
.edit-row .er-save,.edit-row .er-cancel{
border:none;border-radius:4px;padding:0 10px;cursor:pointer;font-size:12.5px;font-weight:600;
}
.edit-row .er-save{background:var(--green-soft);color:#fff;}
.edit-row .er-cancel{background:transparent;border:1px solid var(--border);color:var(--ink-soft);}
.empty-state{
text-align:center;padding:40px 10px;color:var(--ink-soft);font-size:13.5px;
}
.empty-state .big{font-size:26px;display:block;margin-bottom:8px;}
.footer-note{
text-align:center;font-size:11.5px;color:#a9c6e0;margin-top:18px;opacity:0.85;
}
.toolbar{
display:flex;justify-content:center;gap:10px;margin-top:14px;flex-wrap:wrap;
}
.tool-btn{
font-family:'Hind Siliguri','Noto Sans Bengali','Vrinda','Segoe UI',sans-serif;
font-size:12px;color:#bcd7ec;
background:transparent;
border:1px solid rgba(255,255,255,0.35);
border-radius:4px;
padding:7px 12px;
cursor:pointer;
text-decoration:none;
display:inline-block;
}
.tool-btn:hover{border-color:#ffffff;color:#ffffff;}
.tool-btn.danger:hover{border-color:var(--danger);color:var(--danger);}
.tool-btn:focus-visible{outline:2px solid var(--gold);outline-offset:1px;}
/ print report (used for "PDF" export via browser print-to-PDF) /
.print-report{display:none;}
@media print{
body{padding:0;background:#fff;color:#000;}
.masthead,.summary,.budget-box,.charts-section,.catmgmt-box,.combo-box,.dues-box,.entry-box,.ledger,.toolbar,.footer-note{display:none !important;}
.print-report{display:block;}
.print-report table{width:100%;border-collapse:collapse;font-size:12px;}
.print-report th,.print-report td{border:1px solid #999;padding:5px 8px;text-align:left;}
.print-report h1{font-size:20px;margin-bottom:2px;}
.print-report .p-sub{font-size:12px;color:#444;margin-bottom:14px;}
.print-report .p-summary{margin-bottom:16px;font-size:13px;}
}
@media (max-width:480px){
body{padding:16px 10px 50px;}
.summary{grid-template-columns:1fr 1fr;}
.masthead{flex-direction:column;align-items:flex-start;gap:8px;}
.masthead h1{font-size:26px;}
.today-stamp{transform:none;}
:root{--line-h:40px;}
.ledger{padding:6px 8px 6px 40px;}
.ledger::before{left:26px;}
.day-label{font-size:10px;}
.entry-row{font-size:12.5px;gap:4px;}
.entry-row .cat-tag{max-width:52px;font-size:9.5px;padding:1px 5px;}
.entry-row .amt{width:66px;font-size:12px;}
.toolbar{gap:8px;}
.tool-btn{font-size:11px;padding:9px 10px;flex:1 1 auto;text-align:center;min-height:38px;}
.line-item{flex-wrap:wrap;}
.line-item .li-amount{width:calc(50% - 4px);}
.line-item .li-category{width:calc(50% - 4px);}
.line-item .li-note{flex:1 1 100%;order:3;}
.li-remove{order:4;margin-left:auto;}
.charts-grid{gap:10px;}
}
@media (max-width:360px){
.entry-row .cat-tag{display:none;}
}
@media (prefers-reduced-motion: reduce){
.cat-bar-fill,.budget-bar-fill{transition:none;}
}
</style>
</head>
<body>
<div class="wrap">
<div class="masthead">
<div>
<h1>কড়াগণ্ডা</h1>
<div class="sub">স্মার্ট লেজার বুক — আয় ও ব্যয়ের দৈনিক হিসাব</div>
</div>
<div class="head-right">
<div class="today-stamp" id="todayStamp"></div>
<button class="theme-btn" id="themeBtn" title="থিম পরিবর্তন">🌙</button>
</div>
</div>
<div id="reminderBanner"></div>
<div class="summary">
<div class="sum-card balance">
<div class="lbl">বর্তমান ব্যালেন্স</div>
<div class="val" id="balanceVal">৳০</div>
</div>
<div class="sum-card income">
<div class="lbl">এই মাসের আয়</div>
<div class="val" id="monthIncome">৳০</div>
</div>
<div class="sum-card expense">
<div class="lbl">এই মাসের খরচ</div>
<div class="val" id="monthExpense">৳০</div>
</div>
<div class="sum-card">
<div class="lbl">মাসের লাভ/লোকসান</div>
<div class="val" id="monthNet">৳০</div>
</div>
<div class="sum-card">
<div class="lbl">আজকের খরচ</div>
<div class="val" id="todayTotal">৳০</div>
</div>
<div class="sum-card">
<div class="lbl">মোট এন্ট্রি</div>
<div class="val" id="entryCount">০</div>
</div>
<div class="sum-card income">
<div class="lbl">মোট পাওনা</div>
<div class="val" id="sumReceivable">৳০</div>
</div>
<div class="sum-card expense">
<div class="lbl">মোট দেনা</div>
<div class="val" id="sumPayable">৳০</div>
</div>
</div>
<div class="budget-box">
<div class="budget-head">
<h3>মাসিক বাজেট</h3>
<button class="budget-edit-btn" id="budgetEditBtn">বাজেট ঠিক করুন</button>
</div>
<div id="budgetDisplay"></div>
<div class="budget-setup" id="budgetSetup" style="display:none;">
<input type="number" id="budgetInput" placeholder="মাসিক বাজেট (৳)" min="0" step="1">
<button id="budgetSaveBtn">সংরক্ষণ</button>
</div>
</div>
<div class="combo-box">
<div class="combo-head"><h3>টাকা লেখা ও বাকি হিসাব</h3></div>
<div class="combo-tabs">
<button type="button" class="combo-tab active" id="comboTabMoney" data-panel="panelMoney">টাকা লেখা</button>
<button type="button" class="combo-tab" id="comboTabDues" data-panel="panelDues">বাকি হিসাব (পাওনা/দেনা)</button>
</div>
<div class="combo-panel active" id="panelMoney">
<div class="entry-box">
<h3>নতুন এন্ট্রি যোগ করুন</h3>
<div class="type-toggle">
<button type="button" class="active expense" id="typeExpenseBtn">খরচ</button>
<button type="button" class="income" id="typeIncomeBtn">আয়</button>
</div>
<div class="date-row">
<label for="date">তারিখ (সবগুলোর জন্য একই)</label>
<input type="date" id="date">
</div>
<div id="lineItems"></div>
<button type="button" class="add-line-btn" id="addLineBtn">+ আরেকটা যোগ করুন</button>
<button class="add-btn" id="addBtn">সব একসাথে খাতায় লিখুন</button>
</div>
<div class="filter-bar" id="txFilterBar">
<input type="text" class="f-search" id="txSearchInput" placeholder="🔍 নোট বা খাতে খুঁজুন">
<select class="f-type" id="txTypeFilter">
<option value="">সব ধরন</option>
<option value="income">আয়</option>
<option value="expense">খরচ</option>
</select>
<select class="f-category" id="txCategoryFilter">
<option value="">সব খাত</option>
</select>
<input type="date" class="f-date" id="txDateFrom" title="শুরুর তারিখ">
<input type="date" class="f-date" id="txDateTo" title="শেষ তারিখ">
<button type="button" class="f-clear" id="txFilterClear">মুছুন</button>
</div>
<div class="ledger" id="ledger" style="margin-top:18px;"></div>
</div>
<div class="combo-panel" id="panelDues">
<div class="dues-box">
<div class="dues-sub">কে কার কাছে পাবেন/দেবেন তার হিসাব — নাম অনুযায়ী গ্রুপ করা</div>
<div class="dues-mini">
<div class="mini-chip receivable">
<div class="lbl">মোট পাওনা (আপনি পাবেন)</div>
<div class="val" id="totalReceivable">৳০</div>
</div>
<div class="mini-chip payable">
<div class="lbl">মোট দেনা (আপনি দেবেন)</div>
<div class="val" id="totalPayable">৳০</div>
</div>
</div>
<div class="due-type-toggle">
<button type="button" class="active receivable" id="dueReceivableBtn">পাওনা (আমি পাবো)</button>
<button type="button" class="payable" id="duePayableBtn">দেনা (আমি দেবো)</button>
</div>
<div class="due-form-grid">
<input type="text" class="due-name" id="dueName" placeholder="নাম">
<input type="number" class="due-amount" id="dueAmount" placeholder="পরিমাণ (৳)" min="0" step="0.01">
<input type="date" class="due-date" id="dueDate">
<input type="date" class="due-reminder" id="dueReminder" title="রিমাইন্ডার তারিখ (ঐচ্ছিক) — পরিশোধের শেষ তারিখ">
<input type="text" class="due-note" id="dueNote" placeholder="নোট (ঐচ্ছিক)">
</div>
<label style="margin-bottom:10px;font-size:11px;">উপরের তারিখ ঘরে বাকি পরিশোধের শেষ তারিখ দিন — সেই তারিখে বা তার পরে অ্যাপ খুললে মনে করিয়ে দেবে</label>
<button class="add-btn" id="dueAddBtn">বাকি হিসাব যোগ করুন</button>
<div class="filter-bar" id="dueFilterBar">
<input type="text" class="f-search" id="dueSearchInput" placeholder="🔍 নাম দিয়ে খুঁজুন">
<select class="f-type" id="dueTypeFilter">
<option value="">সব</option>
<option value="receivable">পাওনা</option>
<option value="payable">দেনা</option>
</select>
<button type="button" class="f-clear" id="dueFilterClear">মুছুন</button>
</div>
<div class="due-list" id="dueList" style="margin-top:18px;"></div>
</div>
</div>
</div>
<div class="charts-section" id="chartsSection">
<h3>রিপোর্ট ও চার্ট</h3>
<div class="charts-grid">
<div class="chart-card">
<div class="chart-title">এই মাসের খাত অনুযায়ী খরচ</div>
<div id="pieChartHolder"></div>
</div>
<div class="chart-card">
<div class="chart-title">গত ৬ মাসের আয়-ব্যয়</div>
<div id="barChartHolder"></div>
</div>
</div>
</div>
<div class="cat-section" id="catSection" style="display:none;">
<h3>খাত অনুযায়ী খরচ (তালিকা)</h3>
<div id="catBars"></div>
</div>
<div class="catmgmt-box">
<div class="catmgmt-head" id="catmgmtHead">
<h3>খাত ম্যানেজমেন্ট</h3>
<button class="catmgmt-toggle" id="catmgmtToggleBtn">খুলুন ▾</button>
</div>
<div class="catmgmt-body" id="catmgmtBody">
<div class="catmgmt-tabs">
<div class="catmgmt-tab active" data-type="expense">খরচের খাত</div>
<div class="catmgmt-tab" data-type="income">আয়ের খাত</div>
</div>
<div class="catmgmt-list" id="catmgmtList"></div>
<div class="catmgmt-add">
<input type="text" id="catmgmtInput" placeholder="নতুন খাতের নাম">
<button id="catmgmtAddBtn">যোগ করুন</button>
</div>
</div>
</div>
<div class="footer-note">সম্পূর্ণ অফলাইনে চলে — ইন্টারনেট ছাড়াই কাজ করে, তথ্য শুধু এই ব্রাউজারে সংরক্ষিত থাকে</div>
<div class="toolbar">
<button class="tool-btn" id="csvBtn">CSV এক্সপোর্ট</button>
<button class="tool-btn" id="printBtn">PDF রিপোর্ট (প্রিন্ট)</button>
<button class="tool-btn" id="exportBtn">ব্যাকআপ ডাউনলোড</button>
<label class="tool-btn" for="importFile">ব্যাকআপ পুনরুদ্ধার</label>
<input type="file" id="importFile" accept="application/json" hidden>
<button class="tool-btn danger" id="clearBtn">সব মুছে ফেলুন</button>
</div>
<div class="print-report" id="printReport"></div>
</div>
<script>
/ ---------- storage keys ---------- /
const TX_KEY = 'tx:list';
const OLD_KEY = 'expenses:list';
const CAT_KEY = 'tx:categories';
const BUDGET_KEY = 'tx:budget';
const THEME_KEY = 'ui:theme';
const DUES_KEY = 'tx:dues';
let transactions = [];
let categories = { expense: [], income: [] };
let budget = null;
let editingId = null;
let catmgmtType = 'expense';
let catmgmtOpen = false;
let dues = [];
let currentDueType = 'receivable';
let txFilter = { text:'', type:'', category:'', dateFrom:'', dateTo:'' };
let dueFilterState = { text:'', type:'' };
let editingDueId = null;
let payingDueId = null;
const defaultCategories = {
expense: ['খাবার','যাতায়াত','বাসা ভাড়া','কেনাকাটা','চিকিৎসা','বিল/ইউটিলিটি','বিনোদন','শিক্ষা','অন্যান্য'],
income: ['বেতন','ব্যবসা','ফ্রিল্যান্স','উপহার','বিনিয়োগ','অন্যান্য আয়']
};
const palette = ['#2c5f4f','#a97f2e','#b2433a','#8a6a3c','#6a4c93','#c97b3b','#5a8f5a','#9c5c8c','#7a5230','#d19a3a'];
function colorFor(name, type){
const list = categories[type] || [];
const idx = list.indexOf(name);
return palette[(idx < 0 ? 0 : idx) % palette.length];
}
let lineCounter = 0;
let currentFormType = 'expense';
/ ---------- helpers ---------- /
const banglaDigits = ['০','১','২','৩','৪','৫','৬','৭','৮','৯'];
function toBn(num){
const neg = num < 0;
const s = String(Math.round(Math.abs(num))).replace(/\d/g, d => banglaDigits[d]);
return (neg ? '-' : '') + s;
}
function fmtTaka(num){
const neg = num < 0;
return (neg ? '-৳' : '৳') + toBn(Math.abs(num));
}
const banglaMonths = ['জানুয়ারি','ফেব্রুয়ারি','মার্চ','এপ্রিল','মে','জুন','জুলাই','আগস্ট','সেপ্টেম্বর','অক্টোবর','নভেম্বর','ডিসেম্বর'];
const banglaMonthsShort = ['জানু','ফেব্রু','মার্চ','এপ্রিল','মে','জুন','জুলাই','আগস্ট','সেপ্ট','অক্টো','নভে','ডিসে'];
const banglaDays = ['রবি','সোম','মঙ্গল','বুধ','বৃহঃ','শুক্র','শনি'];
function fmtDateLabel(dateStr){
const d = new Date(dateStr + 'T00:00:00');
return
${banglaDays[d.getDay()]}, ${toBn(d.getDate())} ${banglaMonths[d.getMonth()]};}
function todayStr(){
const d = new Date();
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
}
function escCsv(v){
const s = String(v ?? '');
if(/[",\n]/.test(s)) return '"' + s.replace(/"/g,'""') + '"';
return s;
}
/ ---------- persistence ---------- /
function loadAll(){
try{
const rawTx = localStorage.getItem(TX_KEY);
if(rawTx){
transactions = JSON.parse(rawTx);
} else {
const oldRaw = localStorage.getItem(OLD_KEY);
if(oldRaw){
const old = JSON.parse(oldRaw);
transactions = old.map(e => ({ ...e, type: 'expense' }));
saveTx();
} else {
transactions = [];
}
}
}catch(e){ console.error('তথ্য পড়তে সমস্যা', e); transactions = []; }
try{
const rawCat = localStorage.getItem(CAT_KEY);
categories = rawCat ? JSON.parse(rawCat) : JSON.parse(JSON.stringify(defaultCategories));
if(!categories.expense) categories.expense = [...defaultCategories.expense];
if(!categories.income) categories.income = [...defaultCategories.income];
}catch(e){ categories = JSON.parse(JSON.stringify(defaultCategories)); }
try{
const rawBudget = localStorage.getItem(BUDGET_KEY);
budget = rawBudget ? parseFloat(rawBudget) : null;
}catch(e){ budget = null; }
try{
const theme = localStorage.getItem(THEME_KEY) || 'light';
document.documentElement.setAttribute('data-theme', theme);
document.getElementById('themeBtn').textContent = theme === 'dark' ? '☀️' : '🌙';
}catch(e){}
try{
const rawDues = localStorage.getItem(DUES_KEY);
dues = rawDues ? JSON.parse(rawDues) : [];
dues.forEach(d => { if(!Array.isArray(d.payments)) d.payments = []; });
}catch(e){ console.error('বাকি হিসাব পড়তে সমস্যা', e); dues = []; }
}
function saveTx(){
try{ localStorage.setItem(TX_KEY, JSON.stringify(transactions)); }
catch(e){ alert('তথ্য সংরক্ষণ করা যায়নি। ব্রাউজারের স্টোরেজ পূর্ণ থাকতে পারে অথবা প্রাইভেট মোডে আছেন।'); }
}
function saveCategories(){ try{ localStorage.setItem(CAT_KEY, JSON.stringify(categories)); }catch(e){} }
function saveBudget(){ try{ if(budget === null) localStorage.removeItem(BUDGET_KEY); else localStorage.setItem(BUDGET_KEY, String(budget)); }catch(e){} }
function saveDues(){
try{ localStorage.setItem(DUES_KEY, JSON.stringify(dues)); }
catch(e){ alert('বাকি হিসাব সংরক্ষণ করা যায়নি। ব্রাউজারের স্টোরেজ পূর্ণ থাকতে পারে।'); }
}
/ ---------- theme ---------- /
document.getElementById('themeBtn').addEventListener('click', () => {
const cur = document.documentElement.getAttribute('data-theme') || 'light';
const next = cur === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
document.getElementById('themeBtn').textContent = next === 'dark' ? '☀️' : '🌙';
try{ localStorage.setItem(THEME_KEY, next); }catch(e){}
});
/ ---------- category dropdown / line items ---------- /
function categoryOptionsHtml(type){
return categories[type].map(c =>
<option>${c}</option>).join('');}
function createLineItem(){
lineCounter++;
const div = document.createElement('div');
div.className = 'line-item';
div.dataset.lineId = lineCounter;
div.innerHTML =
;<input type="number" class="li-amount" placeholder="পরিমাণ (৳)" min="0" step="0.01">
<select class="li-category">${categoryOptionsHtml(currentFormType)}</select>
<input type="text" class="li-note" placeholder="নোট (ঐচ্ছিক)">
<button type="button" class="li-remove" title="এই লাইন মুছুন">✕</button>
document.getElementById('lineItems').appendChild(div);
updateRemoveButtons();
return div;
}
function updateRemoveButtons(){
const items = document.querySelectorAll('.line-item');
document.querySelectorAll('.li-remove').forEach(btn => { btn.disabled = items.length <= 1; });
}
function resetLineItems(){
document.getElementById('lineItems').innerHTML = '';
createLineItem();
}
function refreshLineItemCategories(){
document.querySelectorAll('#lineItems .li-category').forEach(sel => {
sel.innerHTML = categoryOptionsHtml(currentFormType);
});
}
document.getElementById('lineItems').addEventListener('click', (e) => {
if(e.target.classList.contains('li-remove') && !e.target.disabled){
e.target.closest('.line-item').remove();
updateRemoveButtons();
}
});
document.getElementById('addLineBtn').addEventListener('click', () => {
const newLine = createLineItem();
newLine.querySelector('.li-amount').focus();
});
/ ---------- type toggle ---------- /
function setFormType(type){
currentFormType = type;
document.getElementById('typeExpenseBtn').classList.toggle('active', type === 'expense');
document.getElementById('typeIncomeBtn').classList.toggle('active', type === 'income');
refreshLineItemCategories();
}
document.getElementById('typeExpenseBtn').addEventListener('click', () => setFormType('expense'));
document.getElementById('typeIncomeBtn').addEventListener('click', () => setFormType('income'));
/ ---------- add transactions ---------- /
function addTransactions(){
const dateInput = document.getElementById('date');
const date = dateInput.value || todayStr();
const lineEls = document.querySelectorAll('.line-item');
const newEntries = [];
let firstInvalid = null;
lineEls.forEach(li => {
const amountEl = li.querySelector('.li-amount');
const categoryEl = li.querySelector('.li-category');
const noteEl = li.querySelector('.li-note');
const amount = parseFloat(amountEl.value);
const hasAnyInput = amountEl.value.trim() !== '' || noteEl.value.trim() !== '';
amountEl.style.borderColor = '';
if(!amount || amount <= 0){
if(hasAnyInput && !firstInvalid) firstInvalid = amountEl;
return;
}
newEntries.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2,7) + newEntries.length,
type: currentFormType,
amount: amount,
category: categoryEl.value,
note: noteEl.value.trim(),
date: date,
createdAt: Date.now()
});
});
if(newEntries.length === 0){
const target = firstInvalid || document.querySelector('.li-amount');
target.focus();
target.style.borderColor = '#a83232';
setTimeout(()=> target.style.borderColor = '', 1200);
return;
}
transactions = transactions.concat(newEntries);
saveTx();
resetLineItems();
render();
const firstAmt = document.querySelector('.li-amount');
if(firstAmt) firstAmt.focus();
}
document.getElementById('addBtn').addEventListener('click', addTransactions);
document.getElementById('lineItems').addEventListener('keydown', (e) => {
if(e.key === 'Enter' && (e.target.classList.contains('li-amount') || e.target.classList.contains('li-note'))){
e.preventDefault();
addTransactions();
}
});
/ ---------- delete / edit ---------- /
function deleteTransaction(id){
transactions = transactions.filter(e => e.id !== id);
saveTx();
render();
}
function startEdit(id){
editingId = id;
render();
}
function cancelEdit(){
editingId = null;
render();
}
function saveEdit(id){
const row = document.querySelector(
.edit-row[data-id="${id}"]);if(!row) return;
const amount = parseFloat(row.querySelector('.er-amount').value);
const category = row.querySelector('.er-category').value;
const note = row.querySelector('.er-note').value.trim();
const date = row.querySelector('.er-date').value;
if(!amount || amount <= 0 || !date){
row.querySelector('.er-amount').style.borderColor = '#a83232';
return;
}
const tx = transactions.find(t => t.id === id);
if(tx){ tx.amount = amount; tx.category = category; tx.note = note; tx.date = date; }
saveTx();
editingId = null;
render();
}
/ ---------- category management ---------- /
const catmgmtBody = document.getElementById('catmgmtBody');
document.getElementById('catmgmtToggleBtn').addEventListener('click', () => {
catmgmtOpen = !catmgmtOpen;
catmgmtBody.classList.toggle('open', catmgmtOpen);
document.getElementById('catmgmtToggleBtn').textContent = catmgmtOpen ? 'বন্ধ করুন ▴' : 'খুলুন ▾';
});
document.querySelectorAll('.combo-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.combo-tab').forEach(t => t.classList.toggle('active', t === tab));
const targetId = tab.dataset.panel;
document.querySelectorAll('.combo-panel').forEach(p => p.classList.toggle('active', p.id === targetId));
});
});
document.querySelectorAll('.catmgmt-tab').forEach(tab => {
tab.addEventListener('click', () => {
catmgmtType = tab.dataset.type;
document.querySelectorAll('.catmgmt-tab').forEach(t => t.classList.toggle('active', t === tab));
renderCatMgmt();
});
});
document.getElementById('catmgmtAddBtn').addEventListener('click', () => {
const input = document.getElementById('catmgmtInput');
const name = input.value.trim();
if(!name) { input.focus(); return; }
if(categories[catmgmtType].includes(name)){ input.value=''; return; }
categories[catmgmtType].push(name);
saveCategories();
input.value = '';
renderCatMgmt();
refreshLineItemCategories();
refreshTxCategoryFilter();
render();
});
document.getElementById('catmgmtInput').addEventListener('keydown', (e) => {
if(e.key === 'Enter'){ e.preventDefault(); document.getElementById('catmgmtAddBtn').click(); }
});
function deleteCategory(type, name){
const inUse = transactions.some(t => t.type === type && t.category === name);
if(inUse && !confirm(
"${name}" খাতে এখনো এন্ট্রি আছে। তবুও মুছে ফেলতে চান?)) return;categories[type] = categories[type].filter(c => c !== name);
if(categories[type].length === 0) categories[type].push('অন্যান্য');
saveCategories();
renderCatMgmt();
refreshLineItemCategories();
refreshTxCategoryFilter();
render();
}
function renderCatMgmt(){
const list = document.getElementById('catmgmtList');
list.innerHTML = categories[catmgmtType].map(c =>
).join('');<div class="catmgmt-chip">
<span>${c}</span>
<button data-type="${catmgmtType}" data-name="${c}" title="মুছুন">✕</button>
</div>
list.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => deleteCategory(btn.dataset.type, btn.dataset.name));
});
}
/ ---------- budget ---------- /
document.getElementById('budgetEditBtn').addEventListener('click', () => {
const setup = document.getElementById('budgetSetup');
setup.style.display = setup.style.display === 'none' ? 'flex' : 'none';
document.getElementById('budgetInput').value = budget !== null ? budget : '';
if(setup.style.display === 'flex') document.getElementById('budgetInput').focus();
});
document.getElementById('budgetSaveBtn').addEventListener('click', () => {
const val = parseFloat(document.getElementById('budgetInput').value);
budget = (!isNaN(val) && val > 0) ? val : null;
saveBudget();
document.getElementById('budgetSetup').style.display = 'none';
render();
});
/ ---------- search / filter: টাকা লেখা (ledger) ---------- /
document.getElementById('txSearchInput').addEventListener('input', (e) => {
txFilter.text = e.target.value.trim();
renderLedger();
});
document.getElementById('txTypeFilter').addEventListener('change', (e) => {
txFilter.type = e.target.value;
refreshTxCategoryFilter();
renderLedger();
});
document.getElementById('txCategoryFilter').addEventListener('change', (e) => {
txFilter.category = e.target.value;
renderLedger();
});
document.getElementById('txDateFrom').addEventListener('change', (e) => {
txFilter.dateFrom = e.target.value;
renderLedger();
});
document.getElementById('txDateTo').addEventListener('change', (e) => {
txFilter.dateTo = e.target.value;
renderLedger();
});
document.getElementById('txFilterClear').addEventListener('click', () => {
txFilter = { text:'', type:'', category:'', dateFrom:'', dateTo:'' };
document.getElementById('txSearchInput').value = '';
document.getElementById('txTypeFilter').value = '';
document.getElementById('txDateFrom').value = '';
document.getElementById('txDateTo').value = '';
refreshTxCategoryFilter();
renderLedger();
});
/ ---------- search / filter: বাকি হিসাব ---------- /
document.getElementById('dueSearchInput').addEventListener('input', (e) => {
dueFilterState.text = e.target.value.trim();
renderDues();
});
document.getElementById('dueTypeFilter').addEventListener('change', (e) => {
dueFilterState.type = e.target.value;
renderDues();
});
document.getElementById('dueFilterClear').addEventListener('click', () => {
dueFilterState = { text:'', type:'' };
document.getElementById('dueSearchInput').value = '';
document.getElementById('dueTypeFilter').value = '';
renderDues();
});
/ ---------- dues (বাকি হিসাব) ---------- /
document.getElementById('dueReceivableBtn').addEventListener('click', () => setDueType('receivable'));
document.getElementById('duePayableBtn').addEventListener('click', () => setDueType('payable'));
function setDueType(type){
currentDueType = type;
document.getElementById('dueReceivableBtn').classList.toggle('active', type === 'receivable');
document.getElementById('duePayableBtn').classList.toggle('active', type === 'payable');
}
function dueRemaining(d){
const paid = (d.payments||[]).reduce((s,p)=>s+p.amount,0);
return Math.max(0, d.amount - paid);
}
function duePaidTotal(d){
return (d.payments||[]).reduce((s,p)=>s+p.amount,0);
}
function dueStatus(d){
const remain = dueRemaining(d);
if(remain <= 0) return 'paid';
if(duePaidTotal(d) > 0) return 'partial';
return 'pending';
}
const dueStatusLabel = { paid:'পরিশোধিত', partial:'আংশিক পরিশোধ', pending:'বকেয়া' };
document.getElementById('dueAddBtn').addEventListener('click', () => {
const nameEl = document.getElementById('dueName');
const amountEl = document.getElementById('dueAmount');
const dateEl = document.getElementById('dueDate');
const reminderEl = document.getElementById('dueReminder');
const noteEl = document.getElementById('dueNote');
const name = nameEl.value.trim();
const amount = parseFloat(amountEl.value);
const date = dateEl.value || todayStr();
nameEl.style.borderColor = ''; amountEl.style.borderColor = '';
if(!name){ nameEl.style.borderColor = '#a83232'; nameEl.focus(); return; }
if(!amount || amount <= 0){ amountEl.style.borderColor = '#a83232'; amountEl.focus(); return; }
dues.push({
id: Date.now().toString(36) + Math.random().toString(36).slice(2,7),
person: name,
type: currentDueType,
amount: amount,
date: date,
reminderDate: reminderEl.value || '',
note: noteEl.value.trim(),
createdAt: Date.now(),
payments: []
});
saveDues();
nameEl.value=''; amountEl.value=''; noteEl.value=''; reminderEl.value='';
dateEl.value = todayStr();
render();
});
function startDuePayment(id){
payingDueId = id; editingDueId = null;
render();
}
function cancelDuePayment(){ payingDueId = null; render(); }
function saveDuePayment(id){
const row = document.querySelector(
.payment-form[data-id="${id}"]);if(!row) return;
const amount = parseFloat(row.querySelector('.pf-amount').value);
const date = row.querySelector('.pf-date').value || todayStr();
if(!amount || amount <= 0){ row.querySelector('.pf-amount').style.borderColor = '#a83232'; return; }
const d = dues.find(x => x.id === id);
if(d){
d.payments = d.payments || [];
d.payments.push({ id: Date.now().toString(36)+Math.random().toString(36).slice(2,5), amount, date, createdAt: Date.now() });
}
saveDues();
payingDueId = null;
render();
}
function deletePayment(dueId, paymentId){
const d = dues.find(x => x.id === dueId);
if(!d) return;
d.payments = (d.payments||[]).filter(p => p.id !== paymentId);
saveDues();
render();
}
function startEditDue(id){
editingDueId = id; payingDueId = null;
render();
}
function cancelEditDue(){ editingDueId = null; render(); }
function saveEditDue(id){
const row = document.querySelector(
.due-edit-form[data-id="${id}"]);if(!row) return;
const name = row.querySelector('.ed-name').value.trim();
const type = row.querySelector('.ed-type').value;
const amount = parseFloat(row.querySelector('.ed-amount').value);
const date = row.querySelector('.ed-date').value;
const reminderDate = row.querySelector('.ed-reminder').value || '';
const note = row.querySelector('.ed-note').value.trim();
if(!name || !amount || amount <= 0 || !date) return;
const d = dues.find(x => x.id === id);
if(d){ d.person = name; d.type = type; d.amount = amount; d.date = date; d.reminderDate = reminderDate; d.note = note; }
saveDues();
editingDueId = null;
render();
}
function deleteDue(id){
const d = dues.find(x => x.id === id);
if(d && d.payments && d.payments.length > 0){
if(!confirm('এই হিসাবে পরিশোধের রেকর্ড আছে। তবুও মুছে ফেলতে চান?')) return;
} else if(!confirm('এই বাকি হিসাবটি মুছে ফেলতে চান?')) return;
dues = dues.filter(x => x.id !== id);
saveDues();
render();
}
function applyDueFilters(list){
return list.filter(d => {
if(dueFilterState.type && d.type !== dueFilterState.type) return false;
if(dueFilterState.text && !d.person.toLowerCase().includes(dueFilterState.text.toLowerCase())) return false;
return true;
});
}
let reminderBannerDismissed = false;
function dismissReminderBanner(){
reminderBannerDismissed = true;
document.getElementById('reminderBanner').innerHTML = '';
}
function goToDuesTab(){
document.getElementById('comboTabDues').click();
document.getElementById('comboTabDues').scrollIntoView({ behavior:'smooth', block:'start' });
}
function renderReminderBanner(){
const banner = document.getElementById('reminderBanner');
if(reminderBannerDismissed){ banner.innerHTML = ''; return; }
const today = todayStr();
const due = dues
.filter(d => d.reminderDate && d.reminderDate <= today && dueRemaining(d) > 0)
.sort((a,b) => a.reminderDate.localeCompare(b.reminderDate));
if(due.length === 0){ banner.innerHTML = ''; return; }
const items = due.map(d => {
const overdue = d.reminderDate < today;
const typeLabel = d.type === 'receivable' ? 'পাওনা' : 'দেনা';
return
;<div class="rb-item" style="cursor:pointer;" onclick="goToDuesTab()">
<span class="rb-name">${d.person} <span style="font-weight:400;color:var(--ink-soft);">(${typeLabel})</span></span>
<span class="rb-due">${overdue ? 'মেয়াদ শেষ' : 'আজ'}: ${d.reminderDate}</span>
<span class="rb-amt">${fmtTaka(dueRemaining(d))}</span>
</div>
}).join('');
banner.innerHTML =
;<div class="reminder-banner">
<div class="rb-head">
<span class="rb-title">🔔 ${toBn(due.length)}টি বাকি হিসাবের রিমাইন্ডার তারিখ হয়ে গেছে</span>
<button class="rb-close" onclick="dismissReminderBanner()" title="বন্ধ করুন">✕</button>
</div>
${items}
</div>
}
function renderDues(){
let totalReceivable = 0, totalPayable = 0;
dues.forEach(d => {
const remain = dueRemaining(d);
if(d.type === 'receivable') totalReceivable += remain; else totalPayable += remain;
});
document.getElementById('totalReceivable').textContent = fmtTaka(totalReceivable);
document.getElementById('totalPayable').textContent = fmtTaka(totalPayable);
document.getElementById('sumReceivable').textContent = fmtTaka(totalReceivable);
document.getElementById('sumPayable').textContent = fmtTaka(totalPayable);
const listEl = document.getElementById('dueList');
if(dues.length === 0){
listEl.innerHTML =
<div class="dues-empty">এখনো কোনো বাকি হিসাব নেই। উপরের ফর্ম দিয়ে যোগ করুন।</div>;return;
}
const visibleDues = applyDueFilters(dues);
if(visibleDues.length === 0){
listEl.innerHTML =
<div class="filter-empty">🔍 এই ফিল্টারে কোনো বাকি হিসাব পাওয়া যায়নি।</div>;return;
}
const groups = {};
visibleDues.forEach(d => {
if(!groups[d.person]) groups[d.person] = [];
groups[d.person].push(d);
});
const personNames = Object.keys(groups).sort((a,b) => {
const maxA = Math.max(...groups[a].map(d=>d.createdAt));
const maxB = Math.max(...groups[b].map(d=>d.createdAt));
return maxB - maxA;
});
listEl.innerHTML = personNames.map(person => {
const items = groups[person].slice().sort((a,b)=> b.createdAt - a.createdAt);
let recv = 0, pay = 0;
items.forEach(d => { const r = dueRemaining(d); if(d.type==='receivable') recv += r; else pay += r; });
const net = recv - pay;
const netCls = net > 0 ? 'pos' : (net < 0 ? 'neg' : 'zero');
const netText = net > 0 ?
আপনি পাবেন ${fmtTaka(net)}: (net < 0 ?আপনি দেবেন ${fmtTaka(-net)}: 'হিসাব সমান');const itemsHtml = items.map(d => {
const remain = dueRemaining(d);
const status = dueStatus(d);
const typeLabel = d.type === 'receivable' ? 'পাওনা' : 'দেনা';
if(editingDueId === d.id){
return
;<div class="due-edit-form" data-id="${d.id}">
<input type="text" class="ed-name" value="${d.person.replace(/"/g,'"')}">
<select class="ed-type">
<option value="receivable" ${d.type==='receivable'?'selected':''}>পাওনা</option>
<option value="payable" ${d.type==='payable'?'selected':''}>দেনা</option>
</select>
<input type="number" class="ed-amount" value="${d.amount}" min="0" step="0.01">
<input type="date" class="ed-date" value="${d.date}">
<input type="date" class="ed-reminder" value="${d.reminderDate||''}" title="রিমাইন্ডার তারিখ">
<input type="text" class="ed-note" value="${(d.note||'').replace(/"/g,'"')}" placeholder="নোট">
<div class="pf-actions">
<button class="pf-save" onclick="saveEditDue('${d.id}')">সংরক্ষণ</button>
<button class="pf-cancel" onclick="cancelEditDue()">বাতিল</button>
</div>
</div>
}
const paymentsHtml = (d.payments && d.payments.length > 0) ?
<div class="payment-history">
${d.payments.slice().sort((a,b)=>b.createdAt-a.createdAt).map(p =>
<div class="ph-row">
<span>${p.date} — ${fmtTaka(p.amount)} পরিশোধ হয়েছে</span>
<button onclick="deletePayment('${d.id}','${p.id}')" title="এই পরিশোধ মুছুন">✕</button>
</div>
).join('')}: '';</div>
const paymentFormHtml = (payingDueId === d.id) ?
: '';<div class="payment-form" data-id="${d.id}">
<input type="number" class="pf-amount" placeholder="পরিশোধের পরিমাণ" min="0" step="0.01" max="${remain}">
<input type="date" class="pf-date" value="${todayStr()}">
<div class="pf-actions">
<button class="pf-save" onclick="saveDuePayment('${d.id}')">যোগ করুন</button>
<button class="pf-cancel" onclick="cancelDuePayment()">বাতিল</button>
</div>
</div>
return
<div class="due-row1" style="margin-top:3px;"><span class="due-reminder-tag">⏰ রিমাইন্ডার: ${d.reminderDate}</span></div><div class="due-item">
<div class="due-row1">
<span class="due-type-tag ${d.type}">${typeLabel}</span>
<span class="due-date">${d.date}</span>
<span class="due-note">${d.note ? d.note : '—'}</span>
<span class="status-badge ${status}">${dueStatusLabel[status]}</span>
</div>
${(d.reminderDate && remain > 0) ?
: ''}<button class="pay-btn" onclick="startDuePayment('${d.id}')">পরিশোধ যোগ করুন</button><div class="due-row1" style="margin-top:5px;justify-content:flex-end;">
<span class="due-amt-block"><span class="orig">মূল: ${fmtTaka(d.amount)}</span> <span class="remain">বাকি: ${fmtTaka(remain)}</span></span>
</div>
${paymentsHtml}
${paymentFormHtml}
<div class="due-actions">
${remain > 0 && payingDueId !== d.id ?
: ''};<button onclick="startEditDue('${d.id}')">✎ সম্পাদনা</button>
<button class="due-del" onclick="deleteDue('${d.id}')">✕ মুছুন</button>
</div>
</div>
}).join('');
return
;<div class="person-card">
<div class="person-head">
<span class="p-name">${person}</span>
<span class="p-net ${netCls}">${netText}</span>
</div>
${itemsHtml}
</div>
}).join('');
}
/ ---------- pie chart (SVG) ---------- /
function buildPieSVG(data){
if(data.length === 0) return '<div class="no-data">এই মাসে কোনো খরচ নেই</div>';
const total = data.reduce((s,d)=>s+d.value,0);
const cx=70, cy=70, r=60;
let angle = -Math.PI/2;
let paths = '';
data.forEach(d => {
const frac = d.value/total;
const nextAngle = angle + frac2Math.PI;
const x1 = cx + rMath.cos(angle), y1 = cy + rMath.sin(angle);
const x2 = cx + rMath.cos(nextAngle), y2 = cy + rMath.sin(nextAngle);
const large = (nextAngle-angle) > Math.PI ? 1 : 0;
if(data.length === 1){
paths +=
<circle cx="${cx}" cy="${cy}" r="${r}" fill="${d.color}"></circle>;} else {
paths +=
<path d="M${cx},${cy} L${x1.toFixed(2)},${y1.toFixed(2)} A${r},${r} 0 ${large} 1 ${x2.toFixed(2)},${y2.toFixed(2)} Z" fill="${d.color}"></path>;}
angle = nextAngle;
});
const legend = data.map(d =>
).join('');<div class="legend-row">
<span class="legend-dot" style="background:${d.color}"></span>
<span class="legend-name">${d.name}</span>
<span class="legend-val">${fmtTaka(d.value)}</span>
</div>
return
<div class="pie-wrap">;<svg width="140" height="140" viewBox="0 0 140 140">${paths}</svg>
<div class="pie-legend">${legend}</div>
</div>
}
/ ---------- monthly bar chart (SVG) ---------- /
function buildBarSVG(){
const now = new Date();
const months = [];
for(let i=5;i>=0;i--){
const d = new Date(now.getFullYear(), now.getMonth()-i, 1);
months.push({ y: d.getFullYear(), m: d.getMonth() });
}
const totals = months.map(({y,m}) => {
let inc=0, exp=0;
transactions.forEach(t => {
const d = new Date(t.date+'T00:00:00');
if(d.getFullYear()===y && d.getMonth()===m){
if(t.type==='income') inc += t.amount; else exp += t.amount;
}
});
return { y, m, inc, exp };
});
const maxVal = Math.max(1, ...totals.map(t=>Math.max(t.inc,t.exp)));
const w=280, h=140, padBottom=22, padTop=6, groupW = w/months.length;
let bars = '';
totals.forEach((t, i) => {
const gx = i*groupW;
const barW = groupW*0.32;
const incH = (t.inc/maxVal)*(h-padBottom-padTop);
const expH = (t.exp/maxVal)*(h-padBottom-padTop);
const incX = gx + groupW*0.14;
const expX = gx + groupW*0.54;
bars +=
<rect x="${incX.toFixed(1)}" y="${(h-padBottom-incH).toFixed(1)}" width="${barW.toFixed(1)}" height="${incH.toFixed(1)}" fill="var(--income)" rx="2"></rect>;bars +=
<rect x="${expX.toFixed(1)}" y="${(h-padBottom-expH).toFixed(1)}" width="${barW.toFixed(1)}" height="${expH.toFixed(1)}" fill="var(--margin-red)" rx="2"></rect>;bars +=
<text x="${(gx+groupW/2).toFixed(1)}" y="${h-6}" font-size="9" fill="var(--ink-soft)" text-anchor="middle">${banglaMonthsShort[t.m]}</text>;});
return
<svg width="100%" viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid meet">${bars}</svg>;<div style="display:flex;gap:14px;justify-content:center;margin-top:6px;font-size:11px;color:var(--ink-soft);">
<span><span style="display:inline-block;width:8px;height:8px;background:var(--income);border-radius:2px;margin-right:4px;"></span>আয়</span>
<span><span style="display:inline-block;width:8px;height:8px;background:var(--margin-red);border-radius:2px;margin-right:4px;"></span>খরচ</span>
</div>
}
/ ---------- render ---------- /
function render(){
const now = new Date();
const curMonth = now.getMonth(), curYear = now.getFullYear();
const tStr = todayStr();
renderReminderBanner();
let monthIncome=0, monthExpense=0, todayExpense=0, allIncome=0, allExpense=0;
const catTotals = {};
transactions.forEach(t => {
const d = new Date(t.date+'T00:00:00');
const inMonth = d.getMonth()===curMonth && d.getFullYear()===curYear;
if(t.type==='income'){
allIncome += t.amount;
if(inMonth) monthIncome += t.amount;
} else {
allExpense += t.amount;
if(inMonth){
monthExpense += t.amount;
catTotals[t.category] = (catTotals[t.category]||0) + t.amount;
}
if(t.date === tStr) todayExpense += t.amount;
}
});
const balance = allIncome - allExpense;
document.getElementById('balanceVal').textContent = fmtTaka(balance);
document.getElementById('balanceVal').classList.toggle('neg', balance < 0);
document.getElementById('monthIncome').textContent = fmtTaka(monthIncome);
document.getElementById('monthExpense').textContent = fmtTaka(monthExpense);
const net = monthIncome - monthExpense;
document.getElementById('monthNet').textContent = fmtTaka(net);
document.getElementById('todayTotal').textContent = fmtTaka(todayExpense);
document.getElementById('entryCount').textContent = toBn(transactions.length);
/ budget /
const budgetDisplay = document.getElementById('budgetDisplay');
if(budget === null){
budgetDisplay.innerHTML =
<div class="budget-text">এখনো কোনো বাজেট সেট করা হয়নি।</div>;} else {
const pct = Math.min(100, (monthExpense/budget)*100);
const cls = monthExpense >= budget ? 'over' : (pct >= 80 ? 'warn' : '');
budgetDisplay.innerHTML =
<div class="budget-alert">⚠ বাজেট ${fmtTaka(monthExpense-budget)} অতিক্রম করেছে!</div><div class="budget-bar-bg"><div class="budget-bar-fill ${cls}" style="width:${pct.toFixed(1)}%"></div></div>
<div class="budget-text">${fmtTaka(monthExpense)} খরচ হয়েছে ${fmtTaka(budget)} বাজেটের মধ্যে</div>
${monthExpense > budget ?
: ''}
;}
/ pie chart /
const sortedCats = Object.entries(catTotals).sort((a,b)=>b[1]-a[1]);
const pieData = sortedCats.map(([name,val]) => ({ name, value: val, color: colorFor(name,'expense') }));
document.getElementById('pieChartHolder').innerHTML = buildPieSVG(pieData);
document.getElementById('barChartHolder').innerHTML = buildBarSVG();
/ legacy category bar list /
const catSection = document.getElementById('catSection');
const catBars = document.getElementById('catBars');
if(sortedCats.length === 0){
catSection.style.display = 'none';
} else {
catSection.style.display = 'block';
const maxVal = sortedCats[0][1];
catBars.innerHTML = sortedCats.map(([name, val]) =>
).join('');<div class="cat-row">
<div class="cat-name">${name}</div>
<div class="cat-bar-bg"><div class="cat-bar-fill" style="width:${(val/maxVal*100).toFixed(1)}%; background:${colorFor(name,'expense')}"></div></div>
<div class="cat-amt">${fmtTaka(val)}</div>
</div>
}
renderCatMgmt();
renderLedger();
renderDues();
buildPrintReport(monthIncome, monthExpense, balance, sortedCats);
}
function applyTxFilters(list){
return list.filter(t => {
if(txFilter.type && t.type !== txFilter.type) return false;
if(txFilter.category && t.category !== txFilter.category) return false;
if(txFilter.dateFrom && t.date < txFilter.dateFrom) return false;
if(txFilter.dateTo && t.date > txFilter.dateTo) return false;
if(txFilter.text){
const q = txFilter.text.toLowerCase();
const noteMatch = (t.note||'').toLowerCase().includes(q);
const catMatch = (t.category||'').toLowerCase().includes(q);
if(!noteMatch && !catMatch) return false;
}
return true;
});
}
function isTxFilterActive(){
return !!(txFilter.text || txFilter.type || txFilter.category || txFilter.dateFrom || txFilter.dateTo);
}
function refreshTxCategoryFilter(){
const sel = document.getElementById('txCategoryFilter');
const prev = sel.value;
let list;
if(txFilter.type === 'income') list = categories.income;
else if(txFilter.type === 'expense') list = categories.expense;
else list = Array.from(new Set([...categories.expense, ...categories.income]));
sel.innerHTML =
<option value="">সব খাত</option>+ list.map(c =><option value="${c}">${c}</option>).join('');if(list.includes(prev)) sel.value = prev; else { sel.value=''; txFilter.category=''; }
}
function renderLedger(){
const ledger = document.getElementById('ledger');
if(transactions.length === 0){
ledger.style.background = 'none';
ledger.style.border = 'none';
ledger.innerHTML =
<div class="empty-state"><span class="big">🖋️</span>এখনো কোনো এন্ট্রি লেখা হয়নি।<br>উপরের ফর্ম দিয়ে প্রথম এন্ট্রি যোগ করুন।</div>;return;
}
const visible = applyTxFilters(transactions);
if(visible.length === 0){
ledger.style.background = 'none';
ledger.style.border = 'none';
ledger.innerHTML =
<div class="filter-empty">🔍 এই ফিল্টারে কোনো এন্ট্রি পাওয়া যায়নি।</div>;return;
}
ledger.style.background = '';
ledger.style.border = '';
const grouped = {};
visible.forEach(t => { (grouped[t.date] = grouped[t.date] || []).push(t); });
const dates = Object.keys(grouped).sort((a,b)=> b.localeCompare(a));
ledger.innerHTML = dates.map(date => {
const rows = grouped[date]
.slice()
.sort((a,b)=> b.createdAt - a.createdAt)
.map(t => {
if(editingId === t.id){
const opts = categories[t.type].map(c =>
<option ${c===t.category?'selected':''}>${c}</option>).join('');return
;<div class="edit-row" data-id="${t.id}">
<input type="date" class="er-date" value="${t.date}">
<input type="number" class="er-amount" value="${t.amount}" min="0" step="0.01">
<select class="er-category">${opts}</select>
<input type="text" class="er-note" value="${(t.note||'').replace(/"/g,'"')}" placeholder="নোট">
<div class="er-actions">
<button class="er-save" data-id="${t.id}">সংরক্ষণ</button>
<button class="er-cancel">বাতিল</button>
</div>
</div>
}
const sign = t.type === 'income' ? '+' : '-';
return
;<div class="entry-row" data-id="${t.id}">
<span class="cat-tag">${t.category}</span>
<span class="note">${t.note ? t.note : '—'}</span>
<span class="amt ${t.type}">${sign}${fmtTaka(t.amount).replace('-','')}</span>
<button class="edit" title="সম্পাদনা" data-id="${t.id}">✎</button>
<button class="del" title="মুছে ফেলুন" data-id="${t.id}">✕</button>
</div>
}).join('');
return
<div class="day-group"><div class="day-label">${fmtDateLabel(date)}</div>${rows}</div>;}).join('');
ledger.querySelectorAll('.del').forEach(btn => btn.addEventListener('click', () => deleteTransaction(btn.dataset.id)));
ledger.querySelectorAll('.edit').forEach(btn => btn.addEventListener('click', () => startEdit(btn.dataset.id)));
ledger.querySelectorAll('.er-save').forEach(btn => btn.addEventListener('click', () => saveEdit(btn.dataset.id)));
ledger.querySelectorAll('.er-cancel').forEach(btn => btn.addEventListener('click', cancelEdit));
}
/ ---------- print report ---------- /
function buildPrintReport(monthIncome, monthExpense, balance, sortedCats){
const now = new Date();
const monthLabel =
${banglaMonths[now.getMonth()]} ${toBn(now.getFullYear())};const rows = transactions.slice().sort((a,b)=> b.date.localeCompare(a.date) || b.createdAt-a.createdAt).map(t =>
).join('');<tr>
<td>${t.date}</td>
<td>${t.type==='income' ? 'আয়' : 'খরচ'}</td>
<td>${t.category}</td>
<td>${fmtTaka(t.amount)}</td>
<td>${t.note||''}</td>
</tr>
const catRows = sortedCats.map(([name,val]) =>
<tr><td>${name}</td><td>${fmtTaka(val)}</td></tr>).join('');let totalReceivable = 0, totalPayable = 0;
dues.forEach(d => { const r = dueRemaining(d); if(d.type==='receivable') totalReceivable += r; else totalPayable += r; });
const dueRows = dues.slice().sort((a,b)=> b.createdAt-a.createdAt).map(d =>
).join('');<tr>
<td>${d.person}</td>
<td>${d.type==='receivable' ? 'পাওনা' : 'দেনা'}</td>
<td>${d.date}</td>
<td>${fmtTaka(d.amount)}</td>
<td>${fmtTaka(dueRemaining(d))}</td>
<td>${dueStatusLabel[dueStatus(d)]}</td>
</tr>
document.getElementById('printReport').innerHTML =
;<h1>কড়াগণ্ডা – স্মার্ট লেজার বুক — রিপোর্ট</h1>
<div class="p-sub">তৈরির তারিখ: ${todayStr()} | মাস: ${monthLabel}</div>
<div class="p-summary">
বর্তমান ব্যালেন্স: ${fmtTaka(balance)} |
এই মাসের আয়: ${fmtTaka(monthIncome)} |
এই মাসের খরচ: ${fmtTaka(monthExpense)} |
মোট পাওনা: ${fmtTaka(totalReceivable)} |
মোট দেনা: ${fmtTaka(totalPayable)}
</div>
<h3>এই মাসের খাত অনুযায়ী খরচ</h3>
<table><thead><tr><th>খাত</th><th>পরিমাণ</th></tr></thead><tbody>${catRows || '<tr><td colspan="2">কোনো তথ্য নেই</td></tr>'}</tbody></table>
<h3 style="margin-top:18px;">বাকি হিসাব</h3>
<table><thead><tr><th>নাম</th><th>ধরন</th><th>তারিখ</th><th>মূল পরিমাণ</th><th>বাকি</th><th>অবস্থা</th></tr></thead><tbody>${dueRows || '<tr><td colspan="6">কোনো বাকি হিসাব নেই</td></tr>'}</tbody></table>
<h3 style="margin-top:18px;">সব এন্ট্রি</h3>
<table><thead><tr><th>তারিখ</th><th>ধরন</th><th>খাত</th><th>পরিমাণ</th><th>নোট</th></tr></thead><tbody>${rows || '<tr><td colspan="5">কোনো এন্ট্রি নেই</td></tr>'}</tbody></table>
}
/ ---------- export / import / clear ---------- /
document.getElementById('csvBtn').addEventListener('click', () => {
const header = ['তারিখ','ধরন','খাত','পরিমাণ','নোট'];
const lines = [header.join(',')];
transactions.slice().sort((a,b)=> a.date.localeCompare(b.date)).forEach(t => {
lines.push([t.date, t.type==='income'?'আয়':'খরচ', t.category, t.amount, t.note||''].map(escCsv).join(','));
});
const blob = new Blob(['\uFEFF' + lines.join('\n')], {type:'text/csv;charset=utf-8;'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download =
hisaber-khata-${todayStr()}.csv; a.click();URL.revokeObjectURL(url);
});
document.getElementById('printBtn').addEventListener('click', () => window.print());
document.getElementById('exportBtn').addEventListener('click', () => {
const backup = { transactions, categories, budget, dues };
const blob = new Blob([JSON.stringify(backup, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download =
hisaber-khata-backup-${todayStr()}.json; a.click();URL.revokeObjectURL(url);
});
document.getElementById('importFile').addEventListener('change', (e) => {
const file = e.target.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = () => {
try{
const imported = JSON.parse(reader.result);
let importedTx;
if(Array.isArray(imported)){
importedTx = imported;
} else if(imported && Array.isArray(imported.transactions)){
importedTx = imported.transactions;
if(imported.categories){ categories = imported.categories; saveCategories(); }
if(typeof imported.budget === 'number'){ budget = imported.budget; saveBudget(); }
if(Array.isArray(imported.dues)){
const existingDueIds = new Set(dues.map(x => x.id));
imported.dues.forEach(d => {
if(!d.id || existingDueIds.has(d.id)) d.id = Date.now().toString(36) + Math.random().toString(36).slice(2,7);
if(!Array.isArray(d.payments)) d.payments = [];
});
dues = dues.concat(imported.dues);
saveDues();
}
} else {
throw new Error('invalid format');
}
const valid = importedTx.filter(x => x && typeof x.amount === 'number' && typeof x.date === 'string');
if(valid.length === 0){ alert('এই ফাইলে কোনো সঠিক এন্ট্রি পাওয়া যায়নি।'); return; }
const existingIds = new Set(transactions.map(x => x.id));
valid.forEach(x => {
if(!x.id || existingIds.has(x.id)) x.id = Date.now().toString(36) + Math.random().toString(36).slice(2,7);
if(!x.createdAt) x.createdAt = Date.now();
if(!x.category) x.category = 'অন্যান্য';
if(!x.note) x.note = '';
if(!x.type) x.type = 'expense';
});
transactions = transactions.concat(valid);
saveTx();
refreshLineItemCategories();
refreshTxCategoryFilter();
render();
alert(
${toBn(valid.length)}টি এন্ট্রি পুনরুদ্ধার করা হয়েছে।);}catch(err){
alert('ব্যাকআপ ফাইলটি পড়া যায়নি। ফাইলটি ঠিক আছে কিনা দেখুন।');
}
e.target.value = '';
};
reader.readAsText(file);
});
document.getElementById('clearBtn').addEventListener('click', () => {
if(transactions.length === 0) return;
if(confirm('আপনি কি নিশ্চিত? সব হিসাব স্থায়ীভাবে মুছে যাবে। আগে ব্যাকআপ ডাউনলোড করে নেওয়া ভালো।')){
transactions = [];
saveTx();
render();
}
});
/ ---------- init ---------- /
document.getElementById('date').value = todayStr();
document.getElementById('dueDate').value = todayStr();
loadAll();
createLineItem();
renderCatMgmt();
refreshTxCategoryFilter();
(function stampToday(){
const d = new Date();
document.getElementById('todayStamp').textContent =
${toBn(d.getDate())} ${banglaMonths[d.getMonth()]}, ${toBn(d.getFullYear())};})();
render();
</script>
</body>
</html>