- 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>
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
"""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
|