Skip to content

Commit baf0de1

Browse files
authored
Merge branch 'Prakharrdev:main' into main
2 parents ed69483 + 0dae371 commit baf0de1

11 files changed

Lines changed: 451 additions & 205 deletions

File tree

backend/main.py

Lines changed: 56 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
@@ -1400,9 +1405,59 @@ async def assign_complaint(
14001405
raise HTTPException(status_code=500, detail=f"Assignment failed: {str(e)}")
14011406

14021407

1408+
@app.post("/api/admin/complaints/invalidate")
1409+
async def invalidate_admin_complaints_cache(
1410+
authorization: Optional[str] = Header(None)
1411+
):
1412+
"""Explicitly clear the admin complaints cache (e.g., after an update)."""
1413+
get_citizen_id_from_token(authorization)
1414+
if redis_client:
1415+
try:
1416+
for key in redis_client.scan_iter("admin:complaints:*"):
1417+
redis_client.delete(key)
1418+
except Exception as e:
1419+
print(f"Redis invalidation failed: {e}")
1420+
return {"status": "success"}
1421+
1422+
1423+
# =========================================================
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+
14031458

14041459
# =========================================================
1405-
# 8f. ADMIN COMPLAINTS LIST (Consolidated + Redis)
1460+
# 8g. ADMIN COMPLAINTS LIST (Consolidated + Redis)
14061461
# =========================================================
14071462

14081463

backend/shared.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,12 @@ def get_redis_client() -> Optional[redis.Redis]:
212212
socket_connect_timeout=2,
213213
socket_timeout=2
214214
)
215-
# Verify connection immediately
216-
client.ping()
217-
print(f"SUCCESS: Connected to Redis at {url.split('@')[-1]}")
215+
# We removed the blocking client.ping() here to prevent cold-start delays.
216+
# Connections will be established lazily during the first request.
218217
return client
219218
except Exception as e:
220-
# Hide the actual password in the logs for security
221219
safe_url = url.split('@')[-1] if "@" in url else url
222-
print(f"WARNING: Redis connection failed (URL: {safe_url}): {e}")
220+
print(f"WARNING: Redis client initialization failed (URL: {safe_url}): {e}")
223221
return None
224222

225223
redis_client = get_redis_client()

backend/whatsapp_webhook.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
_find_active_spatial_duplicate,
4343
build_complaint_record,
4444
send_resend_email,
45+
redis_client,
4546
)
4647

4748

@@ -338,6 +339,14 @@ async def confirm_ticket(phone: str, session: dict):
338339
await send_text(phone, f"❌ Failed to submit complaint: {e}")
339340
return
340341

342+
# Invalidate Redis Caches so Admin Dashboard shows new ticket
343+
if redis_client:
344+
try:
345+
for key in redis_client.scan_iter("admin:complaints:*"):
346+
redis_client.delete(key)
347+
except Exception as e:
348+
print(f"Redis invalidation failed: {e}")
349+
341350
if not response.data:
342351
await send_text(phone, "❌ Submission failed. Please try again.")
343352
return

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 */}

0 commit comments

Comments
 (0)