Add admin features, responsive UI, user reviews, visit stats, and channel-colored markers
- Admin: video management with Google Maps match status, manual restaurant mapping, restaurant remap on name change - Admin: user management tab with favorites/reviews detail - Admin: channel deletion fix for IDs with slashes - Frontend: responsive mobile layout (map top, list bottom, 2-row header) - Frontend: channel-colored map markers with legend - Frontend: my reviews list, favorites toggle, visit counter overlay - Frontend: force light mode for dark theme devices - Backend: visit tracking (site_visits table), user reviews endpoint - Backend: bulk transcript/extract streaming, geocode key fixes - Nginx config for production deployment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
93
backend/api/routes/admin_users.py
Normal file
93
backend/api/routes/admin_users.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Admin user management API routes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from core.db import conn
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_users(
|
||||
limit: int = Query(50, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
"""List all users with favorite/review counts."""
|
||||
sql = """
|
||||
SELECT u.id, u.email, u.nickname, u.avatar_url, u.provider, u.created_at,
|
||||
NVL(fav.cnt, 0) AS favorite_count,
|
||||
NVL(rev.cnt, 0) AS review_count
|
||||
FROM tasteby_users u
|
||||
LEFT JOIN (
|
||||
SELECT user_id, COUNT(*) AS cnt FROM user_favorites GROUP BY user_id
|
||||
) fav ON fav.user_id = u.id
|
||||
LEFT JOIN (
|
||||
SELECT user_id, COUNT(*) AS cnt FROM user_reviews GROUP BY user_id
|
||||
) rev ON rev.user_id = u.id
|
||||
ORDER BY u.created_at DESC
|
||||
OFFSET :off ROWS FETCH NEXT :lim ROWS ONLY
|
||||
"""
|
||||
count_sql = "SELECT COUNT(*) FROM tasteby_users"
|
||||
with conn() as c:
|
||||
cur = c.cursor()
|
||||
cur.execute(count_sql)
|
||||
total = cur.fetchone()[0]
|
||||
cur.execute(sql, {"off": offset, "lim": limit})
|
||||
cols = [d[0].lower() for d in cur.description]
|
||||
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||
for r in rows:
|
||||
if r.get("created_at"):
|
||||
r["created_at"] = r["created_at"].isoformat()
|
||||
return {"users": rows, "total": total}
|
||||
|
||||
|
||||
@router.get("/{user_id}/favorites")
|
||||
def get_user_favorites(user_id: str):
|
||||
"""Get a user's favorite restaurants."""
|
||||
sql = """
|
||||
SELECT r.id, r.name, r.address, r.region, r.cuisine_type,
|
||||
r.rating, r.business_status, f.created_at
|
||||
FROM user_favorites f
|
||||
JOIN restaurants r ON r.id = f.restaurant_id
|
||||
WHERE f.user_id = :u
|
||||
ORDER BY f.created_at DESC
|
||||
"""
|
||||
with conn() as c:
|
||||
cur = c.cursor()
|
||||
cur.execute(sql, {"u": user_id})
|
||||
cols = [d[0].lower() for d in cur.description]
|
||||
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||
for r in rows:
|
||||
if r.get("created_at"):
|
||||
r["created_at"] = r["created_at"].isoformat()
|
||||
return rows
|
||||
|
||||
|
||||
@router.get("/{user_id}/reviews")
|
||||
def get_user_reviews(user_id: str):
|
||||
"""Get a user's reviews with restaurant names."""
|
||||
sql = """
|
||||
SELECT r.id, r.restaurant_id, r.rating, r.review_text,
|
||||
r.visited_at, r.created_at,
|
||||
rest.name AS restaurant_name
|
||||
FROM user_reviews r
|
||||
LEFT JOIN restaurants rest ON rest.id = r.restaurant_id
|
||||
WHERE r.user_id = :u
|
||||
ORDER BY r.created_at DESC
|
||||
"""
|
||||
with conn() as c:
|
||||
cur = c.cursor()
|
||||
cur.execute(sql, {"u": user_id})
|
||||
cols = [d[0].lower() for d in cur.description]
|
||||
rows = [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||
for r in rows:
|
||||
# Handle CLOB
|
||||
if hasattr(r.get("review_text"), "read"):
|
||||
r["review_text"] = r["review_text"].read()
|
||||
if r.get("visited_at"):
|
||||
r["visited_at"] = r["visited_at"].isoformat()
|
||||
if r.get("created_at"):
|
||||
r["created_at"] = r["created_at"].isoformat()
|
||||
return rows
|
||||
Reference in New Issue
Block a user