Skip to content

Commit 32690cb

Browse files
authored
Merge pull request #4 from enga018/claude/cycles-till-date-filtering-d48wtg
Fix async races, silent failures, and duplicated exclusion logic
2 parents 8ad0d71 + 8d0ae63 commit 32690cb

1 file changed

Lines changed: 39 additions & 81 deletions

File tree

index.html

Lines changed: 39 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -38,36 +38,8 @@
3838
.brand{display:flex; flex-direction:column; line-height:1.2}
3939
.brand b{font-size:15px; font-weight:650; letter-spacing:.2px}
4040
.brand span{font-size:11px; opacity:.8; text-transform:uppercase; letter-spacing:1px}
41-
.mode-link{
42-
background:rgba(255,255,255,.12); color:#fff; border:1px solid rgba(255,255,255,.25);
43-
padding:7px 12px; border-radius:99px; font-size:12.5px; font-weight:600;
44-
}
45-
.mode-link:active{background:rgba(255,255,255,.22)}
46-
4741
main{max-width:680px; margin:0 auto; padding:18px 16px 60px}
4842

49-
/* Citizen lookup */
50-
.lookup{ text-align:center; padding:14px 4px 6px; }
51-
.lookup h1{ font-size:18px; font-weight:600; margin:0 0 16px; color:var(--ink); }
52-
.searchbar{
53-
display:flex; align-items:center; gap:10px;
54-
max-width:420px; margin:0 auto;
55-
background:var(--surface); border:1px solid var(--border-strong);
56-
border-radius:999px; padding:5px 5px 5px 16px;
57-
transition:border-color .15s, box-shadow .15s;
58-
}
59-
.searchbar:focus-within{ border-color:var(--green); box-shadow:0 0 0 3px rgba(27,77,62,.08); }
60-
.search-ico{ width:18px; height:18px; color:var(--muted); flex:none; }
61-
.searchbar input{
62-
flex:1; min-width:0; border:none; outline:none; background:transparent;
63-
font-size:16px; padding:10px 0; color:var(--ink);
64-
}
65-
.search-go{
66-
flex:none; width:40px; height:40px; border-radius:50%; border:none;
67-
background:var(--green); color:#fff; display:flex; align-items:center; justify-content:center;
68-
}
69-
.search-go svg{ width:18px; height:18px; }
70-
.search-go:active{ background:var(--green-dark); }
7143
.btn{
7244
background:var(--green); color:#fff; border:none; padding:13px 18px;
7345
border-radius:10px; font-size:15px; font-weight:600; white-space:nowrap;
@@ -120,21 +92,6 @@
12092
.amt.payment{color:var(--paid)}
12193
.empty{padding:18px; text-align:center; color:var(--muted); font-size:13.5px}
12294

123-
/* Search result card (step 1) */
124-
.result-card{
125-
display:flex; align-items:center; gap:12px; width:100%; text-align:left;
126-
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
127-
padding:14px 18px; box-shadow:var(--shadow); margin-top:10px; animation:rise .2s ease;
128-
transition:background .15s;
129-
}
130-
.result-card:active{background:var(--paper)}
131-
.result-card .info{flex:1; min-width:0}
132-
.result-card .info b{font-size:16px; display:block}
133-
.result-card .info small{font-size:12.5px; color:var(--muted)}
134-
.result-card .card-amt{font-size:15.5px; font-weight:700; white-space:nowrap; flex:none}
135-
.result-card .card-amt.owe{color:var(--due)}
136-
.result-card .card-amt.clear{color:var(--paid)}
137-
13895
/* Back link */
13996
.back{background:none; border:none; color:var(--green); font-size:13.5px; font-weight:600;
14097
padding:14px 4px 2px; display:inline-flex; align-items:center; gap:4px}
@@ -220,13 +177,6 @@
220177
padding:14px 18px; font-size:13px; font-weight:650; color:var(--green); text-transform:uppercase; letter-spacing:.5px; cursor:pointer}
221178
.dash-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:14px}
222179
.dash-title{font-size:13px; font-weight:650; text-transform:uppercase; letter-spacing:1px; color:var(--muted)}
223-
.card-amt{font-size:15.5px; font-weight:700; white-space:nowrap; flex:none}
224-
.card-amt.owe{color:var(--due)}
225-
.card-amt.clear{color:var(--paid)}
226-
.pager{display:flex; align-items:center; justify-content:space-between; gap:10px; padding:16px 2px 4px}
227-
.pager .pageno{font-size:13px; color:var(--muted)}
228-
.list-head{font-size:12px; text-transform:uppercase; letter-spacing:1px; color:var(--muted); padding:18px 4px 2px}
229-
#citizenList .result-card{margin-top:10px}
230180

231181
footer{text-align:center; color:var(--muted); font-size:11.5px; padding:24px 16px}
232182
.config-warn{background:#fff7e6; border:1px solid #f0d99a; color:#7a5a12; padding:14px 16px; border-radius:10px; font-size:13px; margin:16px; line-height:1.5}
@@ -248,27 +198,6 @@
248198
}
249199
.pending-list .pending-row:last-child{border-bottom:none;}
250200
.pending-list .confirm-btn{background:var(--paid); color:#fff; border:none; padding:4px 12px; border-radius:99px; font-size:12px;}
251-
/* UPI Pay Now button style */
252-
.btn-upi {
253-
background: var(--green);
254-
color: #fff;
255-
border: none;
256-
padding: 11px 22px;
257-
border-radius: 999px;
258-
font-size: 15px;
259-
font-weight: 600;
260-
display: inline-flex;
261-
align-items: center;
262-
justify-content: center;
263-
gap: 8px;
264-
margin-top: 12px;
265-
cursor: pointer;
266-
box-shadow: 0 2px 8px rgba(27,77,62,0.25);
267-
transition: background 0.15s, transform 0.1s;
268-
}
269-
.btn-upi:active { background: var(--green-dark); transform: scale(0.97); }
270-
.btn-upi svg { width: 18px; height: 18px; fill: currentColor; }
271-
.btn-upi, .btn-upi:hover, .btn-upi:visited { text-decoration: none; color: #fff; }
272201
/* styled confirm dialog */
273202
.confirm-overlay{ position:fixed; inset:0; background:rgba(0,0,0,.45); display:flex;
274203
align-items:center; justify-content:center; z-index:9999; padding:24px; }
@@ -383,6 +312,14 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
383312
document.body.appendChild(t); setTimeout(()=>t.remove(),2200);
384313
}
385314
let excludedPeriods=[];
315+
// Single source of truth for "does this charge count toward period-based
316+
// calculations" — used by getEffectiveBalance, unpaidPeriods, and the
317+
// Firebase store's listOwing so exclusion behaves identically everywhere.
318+
// A charge with no note can't match any excluded period name, so it's
319+
// never excluded by this check.
320+
function isChargeExcluded(e){
321+
return !!(e.note && excludedPeriods.includes(e.note));
322+
}
386323
function statusOf(c){
387324
const bal = getEffectiveBalance(c);
388325
if(bal<=0) return "paid";
@@ -399,7 +336,7 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
399336
}
400337
let includedCharged=0;
401338
for(const e of c.ledger){
402-
if(e.type==="charge" && e.note && !excludedPeriods.includes(e.note) && !e.deferred){
339+
if(e.type==="charge" && !e.deferred && !isChargeExcluded(e)){
403340
includedCharged += Number(e.amount || 0);
404341
}
405342
}
@@ -824,7 +761,7 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
824761
let includedCharged=0;
825762
ledSnap.docs.forEach(doc=>{
826763
const e=doc.data();
827-
if(e.type==="charge" && e.note && !excludedPeriods.includes(e.note) && !e.deferred){
764+
if(e.type==="charge" && !e.deferred && !isChargeExcluded(e)){
828765
includedCharged += Number(e.amount || 0);
829766
}
830767
});
@@ -1131,6 +1068,7 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
11311068

11321069
/* ---------- choose backend ---------- */
11331070
let citizensUnsub=null, allCitizens=[], rawCitizens=[], loggedIn=false, store=null, isLocalMode=false;
1071+
let dashboardRefreshSeq=0, dashboardRefreshTimer=null;
11341072

11351073
const notConfigured = Object.values(firebaseConfig).some(v => v === "PASTE_HERE");
11361074
if(notConfigured){
@@ -1175,20 +1113,25 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
11751113
}
11761114
}
11771115

1178-
function onAuth(isIn){
1116+
async function onAuth(isIn){
11791117
loggedIn = isIn;
11801118
if(isIn){
11811119
$("loginCard").classList.add("hidden");
11821120
$("dashboard").classList.remove("hidden");
11831121
// Excluded periods live in Firestore behind admin-only read rules now that
1184-
// there's no public view, so this can only be fetched once logged in.
1185-
store.getExcludedPeriods().then(periods => { excludedPeriods = periods; refreshDashboard(); }).catch(err => console.error("Failed to load excluded periods:", err));
1122+
// there's no public view. Load them *before* subscribing to the citizen
1123+
// list so the first dashboard render already has the right exclusions,
1124+
// instead of a stale/default value that self-corrects a moment later.
1125+
try{ excludedPeriods = await store.getExcludedPeriods(); }
1126+
catch(err){ console.error("Failed to load excluded periods:", err); }
11861127
subscribeCitizens();
11871128
refreshPending();
11881129
} else {
11891130
$("loginCard").classList.remove("hidden");
11901131
$("dashboard").classList.add("hidden");
11911132
if(citizensUnsub){ citizensUnsub(); citizensUnsub=null; }
1133+
clearTimeout(dashboardRefreshTimer);
1134+
dashboardRefreshSeq++; // invalidate any refreshDashboard() still in flight from before logout
11921135
}
11931136
}
11941137

@@ -1199,21 +1142,37 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
11991142
if(citizensUnsub) return;
12001143
citizensUnsub = store.subscribe(list=>{
12011144
rawCitizens = list;
1202-
refreshDashboard();
1145+
scheduleDashboardRefresh();
12031146
});
12041147
}
12051148
// Manage Periods excludes charges by period from balance calculations, but the
12061149
// live citizen list from subscribe() only carries the cached totals, not each
12071150
// household's ledger — so getEffectiveBalance can't apply the exclusion unless
1208-
// we attach the ledger ourselves whenever some period is excluded.
1151+
// we attach the ledger ourselves whenever some period is excluded. Note this
1152+
// means objects in allCitizens only carry .ledger while some period is
1153+
// excluded — any future code reading c.ledger directly (instead of going
1154+
// through getEffectiveBalance/getDeferredAmount, which both guard for its
1155+
// absence) needs to handle it being missing.
1156+
function scheduleDashboardRefresh(){
1157+
// Coalesces bursts of Firestore snapshot events (e.g. a bulk import
1158+
// triggering many rapid writes) into a single ledger refetch instead of
1159+
// one full refetch per event.
1160+
clearTimeout(dashboardRefreshTimer);
1161+
dashboardRefreshTimer = setTimeout(refreshDashboard, 250);
1162+
}
12091163
async function refreshDashboard(){
1164+
const seq = ++dashboardRefreshSeq;
12101165
let list = rawCitizens;
12111166
if(excludedPeriods.length){
12121167
try{
12131168
const ledgers = await store.getAllLedgers();
12141169
list = list.map(c => ({...c, ledger: ledgers[c.id] || []}));
1215-
}catch(e){ console.error("Failed to load ledgers for period exclusion:", e); }
1170+
}catch(e){
1171+
console.error("Failed to load ledgers for period exclusion:", e);
1172+
toast("Couldn't apply period exclusions — showing unfiltered totals");
1173+
}
12161174
}
1175+
if(seq !== dashboardRefreshSeq) return; // superseded by a newer refresh; drop this stale result
12171176
allCitizens = list;
12181177
renderStats(); renderList(); refreshPending();
12191178
}
@@ -1239,8 +1198,7 @@ <h4>Pending UPI payments <span id="pendingCount" class="pending-badge">0</span><
12391198
let pay=0; for(const e of chrono) if(e.type==="payment") pay+=(e.amount||0);
12401199
const owed=[];
12411200
for(const e of chrono){
1242-
if(e.type!=="charge" || e.deferred) continue;
1243-
if(e.note && excludedPeriods.includes(e.note)) continue;
1201+
if(e.type!=="charge" || e.deferred || isChargeExcluded(e)) continue;
12441202
if(pay>=(e.amount||0)){ pay-=(e.amount||0); }
12451203
else { if(e.note) owed.push(e.note); pay=0; }
12461204
}

0 commit comments

Comments
 (0)