Skip to content

Commit 0dae371

Browse files
committed
feat: implement optimistic UI updates, local storage caching, and admin user status management with Redis invalidation
1 parent a8d5705 commit 0dae371

3 files changed

Lines changed: 159 additions & 32 deletions

File tree

backend/main.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ class AdminWorkerCreate(BaseModel):
180180
city: Optional[str] = None
181181
department: str
182182

183+
class AdminUserStatusUpdate(BaseModel):
184+
user_id: str
185+
is_blocked: bool
186+
role: str # 'worker' or 'authority'
187+
183188

184189
class ComplaintAssignRequest(BaseModel):
185190
complaint_id: str
@@ -1416,7 +1421,43 @@ async def invalidate_admin_complaints_cache(
14161421

14171422

14181423
# =========================================================
1419-
# 8f. ADMIN COMPLAINTS LIST (Consolidated + Redis)
1424+
# 8f. ADMIN USER STATUS (Block/Unblock)
1425+
# =========================================================
1426+
1427+
@app.post("/api/admin/users/status")
1428+
async def update_user_status(
1429+
payload: AdminUserStatusUpdate,
1430+
authorization: Optional[str] = Header(None)
1431+
):
1432+
"""Block/Unblock a user and invalidate corresponding Redis cache."""
1433+
await require_admin(authorization)
1434+
1435+
try:
1436+
# Update Profiles table
1437+
await asyncio.to_thread(
1438+
lambda: supabase.table("profiles")
1439+
.update({"is_blocked": payload.is_blocked})
1440+
.eq("id", payload.user_id)
1441+
.execute()
1442+
)
1443+
1444+
# Invalidate Redis based on role
1445+
if redis_client:
1446+
try:
1447+
if payload.role == "worker":
1448+
redis_client.delete("admin:workers:list")
1449+
elif payload.role == "authority":
1450+
redis_client.delete("admin:authorities:list")
1451+
except Exception as e:
1452+
print(f"Redis status invalidation failed: {e}")
1453+
1454+
return {"status": "success", "is_blocked": payload.is_blocked}
1455+
except Exception as e:
1456+
raise HTTPException(status_code=500, detail=f"Failed to update user status: {str(e)}")
1457+
1458+
1459+
# =========================================================
1460+
# 8g. ADMIN COMPLAINTS LIST (Consolidated + Redis)
14201461
# =========================================================
14211462

14221463

frontend/app/admin/Authorities.tsx

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ export default function Authorities({ onViewReports }: { onViewReports?: () => v
121121
});
122122

123123
setAuthorities(mapped as AuthAccount[]);
124+
125+
// Update persistent local cache
126+
localStorage.setItem("admin:authorities:cache", JSON.stringify(mapped));
124127
} catch (err) {
125128
console.error("Dashboard fetch error:", err);
126129
toast.error("Failed to load authorities.");
@@ -130,6 +133,17 @@ export default function Authorities({ onViewReports }: { onViewReports?: () => v
130133
};
131134

132135
useEffect(() => {
136+
// Initial hydration from local storage
137+
const cached = localStorage.getItem("admin:authorities:cache");
138+
if (cached) {
139+
try {
140+
setAuthorities(JSON.parse(cached));
141+
setLoading(false);
142+
} catch (e) {
143+
console.error("Failed to parse authorities cache", e);
144+
}
145+
}
146+
133147
void fetchAuthorities();
134148

135149
const channel = supabase.channel('admin-auth-rt')
@@ -220,24 +234,55 @@ export default function Authorities({ onViewReports }: { onViewReports?: () => v
220234
const handleAssignDept = async () => {
221235
if (!assigningAuth || !selectedNewDept) return;
222236

223-
const updatedCats = assigningAuth.categories.includes(selectedNewDept)
224-
? assigningAuth.categories
225-
: [...assigningAuth.categories, selectedNewDept];
226-
227-
// Attempt pushing sync to profile metadata
228-
const { error } = await (supabase as any).from('profiles').update({
229-
categories: updatedCats,
230-
department: selectedNewDept
231-
}).eq("id", assigningAuth.id);
232-
233-
if (error) {
234-
toast.error("Failed to assign department in database. Refreshing...");
235-
} else {
236-
toast.success(`Assigned ${selectedNewDept} to ${assigningAuth.name}`);
237-
}
238-
239-
void fetchAuthorities();
237+
const workerId = assigningAuth.id;
238+
const previousAuthorities = [...authorities];
239+
240+
// 1. Optimistic UI update
241+
setAuthorities(prev => prev.map(a =>
242+
a.id === workerId ? { ...a, categories: [selectedNewDept] } : a
243+
));
244+
const currentAssigningName = assigningAuth.name;
240245
handleCloseAssignModal();
246+
247+
try {
248+
const { data: { session } } = await supabase.auth.getSession();
249+
if (!session) {
250+
toast.error("Session expired.");
251+
setAuthorities(previousAuthorities);
252+
return;
253+
}
254+
255+
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "https://api.vihaan.perkkk.dev";
256+
const response = await fetch(`${apiUrl}/api/admin/authorities`, {
257+
method: "PATCH",
258+
headers: {
259+
"Content-Type": "application/json",
260+
"Authorization": `Bearer ${session.access_token}`
261+
},
262+
body: JSON.stringify({
263+
authority_id: workerId,
264+
department: selectedNewDept
265+
})
266+
});
267+
268+
if (!response.ok) {
269+
throw new Error("Failed to update department in database");
270+
}
271+
272+
toast.success(`Assigned ${selectedNewDept} to ${currentAssigningName}`);
273+
274+
// Update persistent cache
275+
localStorage.setItem("admin:authorities:cache", JSON.stringify(
276+
authorities.map(a => a.id === workerId ? { ...a, categories: [selectedNewDept] } : a)
277+
));
278+
279+
// Refresh in background
280+
void fetchAuthorities();
281+
} catch (e: any) {
282+
console.error("Assignment failed:", e);
283+
toast.error(e.message || "Failed to assign department");
284+
setAuthorities(previousAuthorities);
285+
}
241286
};
242287

243288
useEffect(() => {
@@ -336,12 +381,18 @@ export default function Authorities({ onViewReports }: { onViewReports?: () => v
336381

337382

338383
return (
339-
<div className={styles.dashboardContainer} ref={containerRef}>
340-
<div className={styles.header}>
341-
<h1 className={styles.title} style={{display:'flex', alignItems:'center', gap: 8}}>
342-
<ShieldCheck size={28}/> Authorities
343-
</h1>
344-
<p className={styles.subtitle}>Manage registered authority accounts.</p>
384+
<div className={styles.dashboardContainer} ref={containerRef} style={{paddingRight: 40}}>
385+
<div className={styles.header} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
386+
<div>
387+
<h1 className={styles.title} style={{display:'flex', alignItems:'center', gap: 8}}>
388+
<ShieldCheck size={28}/> Authorities
389+
</h1>
390+
<p className={styles.subtitle}>Supervise and monitor authority level accounts.</p>
391+
</div>
392+
<div style={{ display: 'flex', gap: '16px', alignItems: 'center', marginTop: 8 }}>
393+
{!loading && <span className={`${styles.cPill} ${styles.resolved}`} style={{ padding: '6px 12px', fontSize: 11, fontWeight: 700 }}>Live Sync Active</span>}
394+
{loading && <span className={`${styles.cPill} ${styles.pending}`} style={{ padding: '6px 12px', fontSize: 11, fontWeight: 700 }}>Updating Cache...</span>}
395+
</div>
345396
</div>
346397

347398
{/* 4-Item KPI Metrics Matrix */}

frontend/app/admin/Workers.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
133133
});
134134

135135
setWorkers(mapped as WorkerAccount[]);
136+
137+
// Update persistent local cache
138+
localStorage.setItem("admin:workers:cache", JSON.stringify(mapped));
136139
} catch (err) {
137140
console.error("Worker fetch error:", err);
138141
toast.error("Failed to load workers.");
@@ -142,6 +145,17 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
142145
};
143146

144147
useEffect(() => {
148+
// Initial hydration from local storage
149+
const cached = localStorage.getItem("admin:workers:cache");
150+
if (cached) {
151+
try {
152+
setWorkers(JSON.parse(cached));
153+
setLoading(false);
154+
} catch (e) {
155+
console.error("Failed to parse workers cache", e);
156+
}
157+
}
158+
145159
void fetchWorkers();
146160

147161
const channel = supabase.channel('admin-worker-rt')
@@ -234,11 +248,19 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
234248

235249

236250
const handleAssignDept = async (workerId: string, department: string) => {
251+
// 1. Optimistic UI: Update local state immediately
252+
const previousWorkers = [...workers];
253+
setWorkers(prev => prev.map(w =>
254+
w.id === workerId ? { ...w, categories: [department] } : w
255+
));
256+
setAssigningWorker(null);
257+
237258
setAssigning(true);
238259
try {
239260
const { data: { session } } = await supabase.auth.getSession();
240261
if (!session) {
241262
toast.error("Session expired.");
263+
setWorkers(previousWorkers); // Rollback
242264
return;
243265
}
244266

@@ -260,11 +282,18 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
260282
}
261283

262284
toast.success(`Assigned ${department} to worker`);
263-
setAssigningWorker(null);
264-
void fetchWorkers(); // Refresh
285+
286+
// Update persistent cache with the new optimistic state
287+
localStorage.setItem("admin:workers:cache", JSON.stringify(
288+
workers.map(w => w.id === workerId ? { ...w, categories: [department] } : w)
289+
));
290+
291+
// Perform background refresh to confirm data consistency
292+
void fetchWorkers();
265293
} catch (e: any) {
266294
console.error("Assignment failed:", e);
267295
toast.error(`Error: ${e.message || "Failed to assign department"}`);
296+
setWorkers(previousWorkers); // Rollback on error
268297
} finally {
269298
setAssigning(false);
270299
}
@@ -361,12 +390,18 @@ export default function Workers({ onViewReports }: { onViewReports?: () => void
361390

362391

363392
return (
364-
<div className={styles.dashboardContainer} ref={containerRef}>
365-
<div className={styles.header}>
366-
<h1 className={styles.title} style={{display:'flex', alignItems:'center', gap: 8}}>
367-
<HardHat size={28}/> Workers
368-
</h1>
369-
<p className={styles.subtitle}>Manage field workers and assignments.</p>
393+
<div className={styles.dashboardContainer} ref={containerRef} style={{paddingRight: 40}}>
394+
<div className={styles.header} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
395+
<div>
396+
<h1 className={styles.title} style={{display:'flex', alignItems:'center', gap: 8}}>
397+
<HardHat size={28}/> Workers
398+
</h1>
399+
<p className={styles.subtitle}>Manage field workers and assignments.</p>
400+
</div>
401+
<div style={{ display: 'flex', gap: '16px', alignItems: 'center', marginTop: 8 }}>
402+
{!loading && <span className={`${styles.cPill} ${styles.resolved}`} style={{ padding: '6px 12px', fontSize: 11, fontWeight: 700 }}>Live Sync Active</span>}
403+
{loading && <span className={`${styles.cPill} ${styles.pending}`} style={{ padding: '6px 12px', fontSize: 11, fontWeight: 700 }}>Updating Cache...</span>}
404+
</div>
370405
</div>
371406

372407
{/* 4-Item KPI Metrics Matrix */}

0 commit comments

Comments
 (0)