diff --git a/apps/tasks/app/admin/page.tsx b/apps/tasks/app/admin/page.tsx new file mode 100644 index 0000000..56a6940 --- /dev/null +++ b/apps/tasks/app/admin/page.tsx @@ -0,0 +1,282 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/lib/auth"; + +interface AdminUser { + id: string; + email: string; + name: string; + phone: string | null; + language: string; + auth_provider: string; + created_at: string; + task_count: number; + goal_count: number; +} + +interface Analytics { + overview: { + total_users: number; + new_users_7d: number; + total_tasks: number; + completed_tasks: number; + tasks_today: number; + total_goals: number; + accepted_invites: number; + ai_messages: number; + errors_24h: number; + total_projects: number; + }; + daily: { day: string; tasks_created: number }[]; +} + +interface ActivityItem { + type: string; + detail: string; + created_at: string; +} + +const API_BASE = typeof window !== "undefined" ? "" : "http://localhost:3000"; + +async function adminFetch(path: string, opts: { method?: string; token?: string } = {}): Promise { + const headers: Record = { "Content-Type": "application/json" }; + if (opts.token) headers["Authorization"] = `Bearer ${opts.token}`; + const res = await fetch(`${API_BASE}${path}`, { method: opts.method || "GET", headers, cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); +} + +export default function AdminPage() { + const { token } = useAuth(); + const router = useRouter(); + const [users, setUsers] = useState([]); + const [analytics, setAnalytics] = useState(null); + const [activity, setActivity] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [tab, setTab] = useState<"overview" | "users" | "activity">("overview"); + + const loadData = useCallback(async () => { + if (!token) return; + setLoading(true); + setError(null); + try { + const [usersRes, analyticsRes, activityRes] = await Promise.all([ + adminFetch<{ data: AdminUser[] }>("/api/v1/admin/users", { token }), + adminFetch<{ data: Analytics }>("/api/v1/admin/analytics", { token }), + adminFetch<{ data: ActivityItem[] }>("/api/v1/admin/activity", { token }), + ]); + setUsers(usersRes.data || []); + setAnalytics(analyticsRes.data || null); + setActivity(activityRes.data || []); + } catch (err) { + console.error("Admin load error:", err); + setError("Failed to load admin data"); + } finally { + setLoading(false); + } + }, [token]); + + useEffect(() => { + if (!token) { router.replace("/login"); return; } + loadData(); + }, [token, router, loadData]); + + async function handleDeleteUser(userId: string) { + if (!token || !confirm("Delete this user and all their data?")) return; + try { + await adminFetch("/api/v1/admin/users/" + userId, { method: "DELETE", token }); + loadData(); + } catch (err) { + console.error("Delete error:", err); + } + } + + async function handleClearErrors() { + if (!token) return; + try { + await adminFetch("/api/v1/admin/errors/clear", { method: "DELETE", token }); + loadData(); + } catch (err) { + console.error("Clear errors:", err); + } + } + + const o = analytics?.overview; + const maxTasks = analytics?.daily ? Math.max(...analytics.daily.map(d => Number(d.tasks_created)), 1) : 1; + + function formatDate(d: string) { + return new Date(d).toLocaleDateString("cs-CZ", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }); + } + + function typeLabel(t: string) { + const map: Record = { + task_created: "New task", + user_registered: "New user", + goal_created: "New goal", + invite_sent: "Invitation", + }; + return map[t] || t; + } + + function typeColor(t: string) { + const map: Record = { + task_created: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300", + user_registered: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300", + goal_created: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300", + invite_sent: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300", + }; + return map[t] || "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"; + } + + if (loading) { + return ( +
+
+

Loading admin data...

+
+ ); + } + + if (error) { + return ( +
+

{error}

+ +
+ ); + } + + return ( +
+

Admin Dashboard

+ + {/* Tab bar */} +
+ {(["overview", "users", "activity"] as const).map((t) => ( + + ))} +
+ + {/* Overview tab */} + {tab === "overview" && o && ( +
+ {/* Stat cards */} +
+ {[ + { label: "Users", value: o.total_users, sub: `+${o.new_users_7d} this week`, color: "text-blue-600 dark:text-blue-400" }, + { label: "Tasks", value: o.total_tasks, sub: `${o.completed_tasks} completed`, color: "text-green-600 dark:text-green-400" }, + { label: "Today", value: o.tasks_today, sub: "tasks created", color: "text-purple-600 dark:text-purple-400" }, + { label: "Goals", value: o.total_goals, sub: "total", color: "text-orange-600 dark:text-orange-400" }, + { label: "Projects", value: o.total_projects, sub: "active", color: "text-indigo-600 dark:text-indigo-400" }, + { label: "AI msgs", value: o.ai_messages, sub: "chat responses", color: "text-pink-600 dark:text-pink-400" }, + { label: "Invites", value: o.accepted_invites, sub: "accepted", color: "text-teal-600 dark:text-teal-400" }, + { label: "Errors", value: o.errors_24h, sub: "last 24h", color: o.errors_24h > 0 ? "text-red-600 dark:text-red-400" : "text-green-600 dark:text-green-400" }, + ].map((card) => ( +
+

{card.label}

+

{card.value}

+

{card.sub}

+
+ ))} +
+ + {/* Daily activity chart */} + {analytics?.daily && analytics.daily.length > 0 && ( +
+

Tasks Created (last 7 days)

+
+ {analytics.daily.map((d) => ( +
+ {Number(d.tasks_created)} +
+ + {new Date(d.day).toLocaleDateString("cs-CZ", { day: "numeric", month: "numeric" })} + +
+ ))} +
+
+ )} + + {/* Clear errors button */} + {o.errors_24h > 0 && ( + + )} +
+ )} + + {/* Users tab */} + {tab === "users" && ( +
+

{users.length} registered users

+ {users.map((u) => ( +
+
+ {(u.name || u.email || "?").charAt(0).toUpperCase()} +
+
+

{u.name || "No name"}

+

{u.email}

+
+ {u.task_count} tasks + {u.goal_count} goals + {u.auth_provider || "email"} + {u.language || "cs"} +
+
+ +
+ ))} +
+ )} + + {/* Activity tab */} + {tab === "activity" && ( +
+ {activity.length === 0 ? ( +

No recent activity

+ ) : ( + activity.map((a, i) => ( +
+ + {typeLabel(a.type)} + + {a.detail} + {formatDate(a.created_at)} +
+ )) + )} +
+ )} +
+ ); +}