import { createContext, useContext, useState, useCallback } from "react"; import api from "@/utils/api.util"; import { toast } from "sonner"; const UserContext = createContext(null); export const useUsers = () => { const ctx = useContext(UserContext); if (!ctx) throw new Error("useUsers must be used inside UserProvider"); return ctx; }; const PAGINATION_INIT = { page: 1, limit: 10, totalRecords: 0, totalPages: 0, hasPrevPage: false, hasNextPage: false, }; const BASE = "/admin"; export const UserProvider = ({ children }) => { const [users, setUsers] = useState([]); const [user, setUser] = useState(null); const [sessions, setSessions] = useState([]); const [achievements, setAchievements] = useState([]); const [achievementsLoading, setAchievementsLoading] = useState(false); const [pagination, setPagination] = useState(PAGINATION_INIT); const [attributes, setAttributes] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [activity, setActivity] = useState([]); const [activityPagination, setActivityPagination] = useState(PAGINATION_INIT); const [activityLoading, setActivityLoading] = useState(false); const [bans, setBans] = useState([]); const [bansLoading, setBansLoading] = useState(false); const request = useCallback(async (fn) => { setLoading(true); setError(null); try { return await fn(); } catch (err) { const message = err?.response?.data?.message || err.message || "Something went wrong."; setError(message); toast.error(message); return null; } finally { setLoading(false); } }, []); // ─── GET /api/admin/users ────────────────────────────────────────────────── const fetchUsers = useCallback( ({ page = 1, limit = 10, filters = [], sort = [] } = {}) => request(async () => { const { data } = await api.get(`${BASE}/users`, { params: { page, limit, filters: filters.length ? JSON.stringify(filters) : undefined, sort: sort.length ? JSON.stringify(sort) : undefined, }, }); const final_data = data?.data; setUsers(final_data?.data ?? []); setPagination(final_data?.pagination ?? PAGINATION_INIT); setAttributes(final_data?.attributes ?? []); return data?.data; }), [request] ); // ─── GET /api/admin/users/archived ──────────────────────────────────────── const fetchArchivedUsers = useCallback( ({ page = 1, limit = 10, filters = [], sort = [] } = {}) => request(async () => { const { data } = await api.get(`${BASE}/users/archived`, { params: { page, limit, filters: filters.length ? JSON.stringify(filters) : undefined, sort: sort.length ? JSON.stringify(sort) : undefined, }, }); const final_data = data?.data; setUsers(final_data?.data ?? []); setPagination(final_data?.pagination ?? PAGINATION_INIT); setAttributes(final_data?.attributes ?? []); return data?.data; }), [request] ); // ─── GET /api/admin/users/:id ───────────────────────────────────────────── const fetchUser = useCallback( (userId) => request(async () => { const res = await api.get(`${BASE}/users/${userId}`); setUser(res.data?.data ?? null); return res.data; }), [request] ); // ─── POST /api/admin/users/staff ────────────────────────────────────────── const addStaffUser = useCallback( (payload) => request(async () => { const res = await api.post(`${BASE}/users/staff`, payload); toast.success("Staff user added successfully."); return res.data; }), [request] ); // ─── PUT /api/admin/users/:id ───────────────────────────────────────────── const updateUser = useCallback( (userId, payload) => request(async () => { const res = await api.put(`${BASE}/users/${userId}`, payload); setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, ...res.data?.data } : u)) ); toast.success("User updated successfully."); return res.data; }), [request] ); // ─── DELETE /api/admin/users/:id ────────────────────────────────────────── const deactivateUser = useCallback( (userId) => request(async () => { const res = await api.delete(`${BASE}/users/${userId}`); setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, is_active: false } : u)) ); toast.success("User deactivated."); return res.data; }), [request] ); // ─── DELETE /api/admin/users/bulk ───────────────────────────────────────── const deactivateUsers = useCallback( ({ ids }) => request(async () => { const res = await api.delete(`${BASE}/users/bulk`, { data: { ids } }); const { deactivated_ids } = res.data?.data ?? {}; if (deactivated_ids?.length) { setUsers((prev) => prev.map((u) => deactivated_ids.includes(u.user_id) ? { ...u, is_active: false } : u ) ); toast.success(`${deactivated_ids.length} user(s) deactivated.`); } return res.data; }), [request] ); // ─── POST /api/admin/users/:id/restore ──────────────────────────────────── const restoreUser = useCallback( (userId) => request(async () => { const res = await api.post(`${BASE}/users/${userId}/restore`); setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, is_active: true } : u)) ); toast.success("User restored."); return res.data; }), [request] ); // ─── POST /api/admin/users/bulk/restore ─────────────────────────────────── const restoreUsers = useCallback( ({ ids }) => request(async () => { const res = await api.post(`${BASE}/users/bulk/restore`, { ids }); const { restored_ids } = res.data?.data ?? {}; if (restored_ids?.length) { setUsers((prev) => prev.map((u) => restored_ids.includes(u.user_id) ? { ...u, is_active: true } : u ) ); toast.success(`${restored_ids.length} user(s) restored.`); } return res.data; }), [request] ); // ─── GET /api/admin/users/:id/sessions ──────────────────────────────────── const fetchUserSessions = useCallback( (userId) => request(async () => { const res = await api.get(`${BASE}/users/${userId}/sessions`); setSessions(res.data?.data ?? []); return res.data; }), [request] ); // ─── DELETE /api/admin/users/:id/sessions/:sid ──────────────────────────── const terminateSession = useCallback( (userId, sessionId) => request(async () => { const res = await api.delete(`${BASE}/users/${userId}/sessions/${sessionId}`); setSessions((prev) => prev.map((s) => (s.session_id === sessionId ? { ...s, is_active: false } : s)) ); toast.success("Session terminated."); return res.data; }), [request] ); // ─── GET /api/admin/activity ────────────────────────────────────────────── const fetchActivity = useCallback( ({ page = 1, limit = 20, action = undefined, from = undefined, to = undefined } = {}) => request(async () => { const res = await api.get(`${BASE}/activity`, { params: { page, limit, action, from, to } }); const d = res.data?.data; setActivity(d?.activities ?? []); setActivityPagination({ page: d?.page ?? 1, limit, totalRecords: d?.total ?? 0, totalPages: d?.totalPages ?? 0, hasPrevPage: (d?.page ?? 1) > 1, hasNextPage: (d?.page ?? 1) < (d?.totalPages ?? 0), }); return d; }), [request] ); // ─── GET /api/admin/users/:id/activity ──────────────────────────────────── const fetchUserActivity = useCallback( async (userId, { page = 1, limit = 20, action = undefined } = {}) => { setActivityLoading(true); try { const res = await api.get(`${BASE}/users/${userId}/activity`, { params: { page, limit, action } }); const d = res.data?.data; setActivity(d?.activities ?? []); setActivityPagination({ page: d?.page ?? 1, limit, totalRecords: d?.total ?? 0, totalPages: d?.totalPages ?? 0, hasPrevPage: (d?.page ?? 1) > 1, hasNextPage: (d?.page ?? 1) < (d?.totalPages ?? 0), }); return d; } catch (err) { toast.error(err?.response?.data?.message ?? "Could not load activity."); return null; } finally { setActivityLoading(false); } }, [] ); // ─── GET /api/admin/users/field-values ──────────────────────────────────── const fetchUserFieldValues = useCallback( (field) => request(async () => { const res = await api.get(`${BASE}/users/field-values`, { params: { field } }); return res.data?.data; }), [request] ); // ─── GET /api/admin/users/:id/achievements ──────────────────────────────── const fetchUserAchievements = useCallback(async (userId) => { setAchievementsLoading(true); try { const res = await api.get(`${BASE}/users/${userId}/achievements`); setAchievements(res.data?.data ?? []); return res.data?.data; } catch (err) { toast.error(err?.response?.data?.message ?? "Could not load achievements."); return []; } finally { setAchievementsLoading(false); } }, []); // ─── POST /api/admin/users/:id/ban ──────────────────────────────────────── const banUser = useCallback( (userId, payload) => request(async () => { const res = await api.post(`${BASE}/users/${userId}/ban`, payload); setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, is_banned: true } : u)) ); if (user?.user_id === userId) setUser((prev) => prev ? { ...prev, is_banned: true } : prev); toast.success("User banned successfully."); return res.data; }), [request, user] ); // ─── POST /api/admin/users/:id/unban ────────────────────────────────────── const unbanUser = useCallback( (userId, payload) => request(async () => { const res = await api.post(`${BASE}/users/${userId}/unban`, payload); setUsers((prev) => prev.map((u) => (u.user_id === userId ? { ...u, is_banned: false } : u)) ); if (user?.user_id === userId) setUser((prev) => prev ? { ...prev, is_banned: false } : prev); toast.success("User unbanned successfully."); return res.data; }), [request, user] ); // ─── POST /api/admin/users/bulk/ban ─────────────────────────────────────── const bulkBanUsers = useCallback( ({ ids, ...payload }) => request(async () => { const res = await api.post(`${BASE}/users/bulk/ban`, { ids, ...payload }); const { banned_ids } = res.data?.data ?? {}; if (banned_ids?.length) { setUsers((prev) => prev.map((u) => (banned_ids.includes(u.user_id) ? { ...u, is_banned: true } : u)) ); toast.success(`${banned_ids.length} user(s) banned.`); } return res.data; }), [request] ); // ─── POST /api/admin/users/bulk/unban ───────────────────────────────────── const bulkUnbanUsers = useCallback( ({ ids, lift_reason } = {}) => request(async () => { const res = await api.post(`${BASE}/users/bulk/unban`, { ids, lift_reason }); const { unbanned_ids } = res.data?.data ?? {}; if (unbanned_ids?.length) { setUsers((prev) => prev.map((u) => (unbanned_ids.includes(u.user_id) ? { ...u, is_banned: false } : u)) ); toast.success(`${unbanned_ids.length} user(s) unbanned.`); } return res.data; }), [request] ); // ─── GET /api/admin/users/:id/bans ──────────────────────────────────────── const fetchUserBans = useCallback(async (userId) => { setBansLoading(true); try { const res = await api.get(`${BASE}/users/${userId}/bans`); setBans(res.data?.data ?? []); return res.data?.data; } catch (err) { toast.error(err?.response?.data?.message ?? "Could not load ban history."); return []; } finally { setBansLoading(false); } }, []); return ( {children} ); };