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;
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 }
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 }
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}
385314let 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+ }
386323function 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 ---------- */
11331070let citizensUnsub = null , allCitizens = [ ] , rawCitizens = [ ] , loggedIn = false , store = null , isLocalMode = false ;
1071+ let dashboardRefreshSeq = 0 , dashboardRefreshTimer = null ;
11341072
11351073const notConfigured = Object . values ( firebaseConfig ) . some ( v => v === "PASTE_HERE" ) ;
11361074if ( 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+ }
12091163async 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