451 lines
12 KiB
TypeScript
451 lines
12 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
|
export interface DashboardStats {
|
|
totalUsers: number;
|
|
activeClients: number;
|
|
totalRevenue: number;
|
|
revenueGrowth: number;
|
|
}
|
|
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
role: string;
|
|
phone?: string;
|
|
gymId?: string;
|
|
gymName?: string | null;
|
|
createdAt?: string;
|
|
isCheckedIn?: boolean;
|
|
checkInTime?: string;
|
|
lastCheckInTime?: string;
|
|
checkInsThisWeek?: number;
|
|
checkInsThisMonth?: number;
|
|
client?: {
|
|
id: string;
|
|
membershipType: string;
|
|
membershipStatus: string;
|
|
joinDate: string;
|
|
lastVisit?: string;
|
|
};
|
|
}
|
|
|
|
export interface Recommendation {
|
|
id: string;
|
|
userId: string;
|
|
type: string;
|
|
title: string;
|
|
description: string;
|
|
content?: string;
|
|
recommendationText?: string;
|
|
activityPlan?: string;
|
|
dietPlan?: string;
|
|
status: "pending" | "approved" | "rejected";
|
|
createdAt: string;
|
|
user?: User;
|
|
}
|
|
|
|
export interface Gym {
|
|
id: string;
|
|
name: string;
|
|
address?: string;
|
|
}
|
|
|
|
export interface AttendanceRecord {
|
|
id: string;
|
|
userId: string;
|
|
checkIn: string;
|
|
checkOut?: string;
|
|
date: string;
|
|
type?: string;
|
|
}
|
|
|
|
export interface Invitation {
|
|
id: string;
|
|
emailAddress: string;
|
|
publicMetadata: {
|
|
role?: string;
|
|
gymId?: string;
|
|
createdBy?: string;
|
|
} | null;
|
|
status: "pending" | "accepted" | "revoked" | "expired";
|
|
url?: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
revoked?: boolean;
|
|
}
|
|
|
|
async function fetchApi<T>(url: string, options?: RequestInit): Promise<T> {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...options?.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response
|
|
.json()
|
|
.catch(() => ({ error: "Request failed" }));
|
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export function useDashboardStats(gymId?: string) {
|
|
return useQuery({
|
|
queryKey: ["dashboard-stats", gymId],
|
|
queryFn: () => {
|
|
const url = gymId
|
|
? `/api/admin/stats?gymId=${gymId}`
|
|
: "/api/admin/stats";
|
|
return fetchApi<{ data: { stats: DashboardStats } }>(url).then(
|
|
(res) => res.data?.stats,
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUsers(filters?: {
|
|
role?: string;
|
|
gymId?: string;
|
|
search?: string;
|
|
}) {
|
|
const params = new URLSearchParams();
|
|
if (filters?.role) params.set("role", filters.role);
|
|
if (filters?.gymId) params.set("gymId", filters.gymId);
|
|
if (filters?.search) params.set("search", filters.search);
|
|
|
|
const queryString = params.toString();
|
|
const url = queryString ? `/api/users?${queryString}` : "/api/users";
|
|
|
|
return useQuery({
|
|
queryKey: ["users", filters],
|
|
queryFn: () =>
|
|
fetchApi<{ data: { users: User[] } }>(url).then(
|
|
(res) => res.data?.users ?? [],
|
|
),
|
|
});
|
|
}
|
|
|
|
export function useUser(userId: string) {
|
|
return useQuery({
|
|
queryKey: ["user", userId],
|
|
queryFn: () =>
|
|
fetchApi<{ data: { user: User } }>(`/api/users?id=${userId}`).then(
|
|
(res) => res.data?.user,
|
|
),
|
|
enabled: !!userId,
|
|
});
|
|
}
|
|
|
|
export function useRecommendations(filters?: {
|
|
userId?: string;
|
|
status?: string;
|
|
}) {
|
|
const params = new URLSearchParams();
|
|
if (filters?.userId) params.set("userId", filters.userId);
|
|
if (filters?.status) params.set("status", filters.status);
|
|
|
|
const queryString = params.toString();
|
|
const url = queryString
|
|
? `/api/recommendations?${queryString}`
|
|
: "/api/recommendations";
|
|
|
|
return useQuery({
|
|
queryKey: ["recommendations", filters],
|
|
queryFn: () =>
|
|
fetchApi<{ data: { recommendations: Recommendation[] } }>(url).then(
|
|
(res) => res.data?.recommendations ?? [],
|
|
),
|
|
});
|
|
}
|
|
|
|
export function usePendingRecommendationsCount() {
|
|
return useQuery({
|
|
queryKey: ["pending-recommendations-count"],
|
|
queryFn: () =>
|
|
fetchApi<{ data: { recommendations: Recommendation[] } }>(
|
|
"/api/recommendations",
|
|
).then(
|
|
(res) =>
|
|
(res.data?.recommendations ?? []).filter(
|
|
(r) => r.status === "pending",
|
|
).length,
|
|
),
|
|
refetchInterval: 30000,
|
|
});
|
|
}
|
|
|
|
export function useGyms() {
|
|
return useQuery({
|
|
queryKey: ["gyms"],
|
|
queryFn: () =>
|
|
fetchApi<{ data: { gyms: Gym[] } }>("/api/gyms").then(
|
|
(res) => res.data?.gyms ?? (Array.isArray(res) ? res : []),
|
|
),
|
|
});
|
|
}
|
|
|
|
export function useAttendance(filters?: { userId?: string; date?: string }) {
|
|
const params = new URLSearchParams();
|
|
if (filters?.userId) params.set("userId", filters.userId);
|
|
if (filters?.date) params.set("date", filters.date);
|
|
|
|
const queryString = params.toString();
|
|
const url = queryString
|
|
? `/api/admin/attendance?${queryString}`
|
|
: "/api/admin/attendance";
|
|
|
|
return useQuery({
|
|
queryKey: ["attendance", filters],
|
|
queryFn: () =>
|
|
fetchApi<{ data: { records: AttendanceRecord[] } }>(url).then(
|
|
(res) => res.data?.records ?? [],
|
|
),
|
|
});
|
|
}
|
|
|
|
export function useCreateUser() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: {
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
role: string;
|
|
phone?: string;
|
|
gymId?: string;
|
|
}) =>
|
|
fetchApi<{ data: { userId: string } }>("/api/users/create", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}).then((res) => res.data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["users"] });
|
|
queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateUser() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: { id: string; [key: string]: unknown }) =>
|
|
fetchApi<{ data: { success: boolean } }>(`/api/users?id=${data.id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: (_, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: ["users"] });
|
|
queryClient.invalidateQueries({ queryKey: ["user", variables.id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteUser() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (userId: string) =>
|
|
fetchApi<{ data: { success: boolean } }>(`/api/users?id=${userId}`, {
|
|
method: "DELETE",
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["users"] });
|
|
queryClient.invalidateQueries({ queryKey: ["dashboard-stats"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useGenerateRecommendations() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (userId?: string) =>
|
|
fetchApi<{
|
|
data: { success: boolean; recommendations?: Recommendation[] };
|
|
}>("/api/recommendations/generate", {
|
|
method: "POST",
|
|
body: userId ? JSON.stringify({ userId }) : undefined,
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["recommendations"] });
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["pending-recommendations-count"],
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useApproveRecommendation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: { recommendationId: string; approved: boolean }) =>
|
|
fetchApi<{ data: { success: boolean } }>("/api/recommendations/approve", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["recommendations"] });
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["pending-recommendations-count"],
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateRecommendation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: {
|
|
id: string;
|
|
content?: string;
|
|
activityPlan?: string;
|
|
dietPlan?: string;
|
|
}) =>
|
|
fetchApi<{ data: { success: boolean } }>("/api/recommendations", {
|
|
method: "PUT",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["recommendations"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCheckIn() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (userId: string) =>
|
|
fetchApi<{ data: { success: boolean; record: AttendanceRecord } }>(
|
|
"/api/attendance/check-in",
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({ userId }),
|
|
},
|
|
),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useCheckOut() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (userId: string) =>
|
|
fetchApi<{ data: { success: boolean } }>("/api/attendance/check-out", {
|
|
method: "POST",
|
|
body: JSON.stringify({ userId }),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useInvitations(gymId?: string) {
|
|
return useQuery({
|
|
queryKey: ["invitations", gymId],
|
|
queryFn: () => {
|
|
const url = gymId
|
|
? `/api/invitations?gymId=${gymId}`
|
|
: "/api/invitations";
|
|
return fetchApi<{ data: { invitations: Invitation[] } }>(url).then(
|
|
(res) => res.data?.invitations ?? [],
|
|
);
|
|
},
|
|
refetchInterval: (query) => {
|
|
const data = query.state.data;
|
|
const hasData = data && data.length > 0;
|
|
const fetchCount = query.state.dataUpdateCount;
|
|
|
|
// Poll every 2 seconds if:
|
|
// 1. No invitations returned yet
|
|
// 2. Haven't exceeded 5 attempts (10 seconds total)
|
|
if (!hasData && fetchCount < 5) {
|
|
return 2000;
|
|
}
|
|
|
|
// Stop polling after 10 seconds or when data is present
|
|
return false;
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useSendInvitation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: { email: string; role: string }) =>
|
|
fetchApi<{ data: { success: boolean } }>("/api/invitations", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["invitations"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useRevokeInvitation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (invitationId: string) =>
|
|
fetchApi<{ data: { success: boolean } }>(
|
|
`/api/invitations/${invitationId}`,
|
|
{ method: "DELETE" },
|
|
),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["invitations"] });
|
|
queryClient.invalidateQueries({ queryKey: ["users"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useResendInvitation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (invitationId: string) =>
|
|
fetchApi<{ data: { invitation: any } }>(
|
|
`/api/invitations/${invitationId}/resend`,
|
|
{ method: "POST" },
|
|
),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["invitations"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export interface AnalyticsData {
|
|
userGrowth: { label: string; value: number }[];
|
|
membershipDistribution: { label: string; value: number; color: string }[];
|
|
revenue: { label: string; value: number; color: string }[];
|
|
}
|
|
|
|
export function useAnalytics(months: number = 6, gymId?: string) {
|
|
return useQuery({
|
|
queryKey: ["analytics", months, gymId],
|
|
queryFn: () => {
|
|
const url = gymId
|
|
? `/api/admin/analytics?months=${months}&gymId=${gymId}`
|
|
: `/api/admin/analytics?months=${months}`;
|
|
return fetchApi<{ data: { analytics: AnalyticsData } }>(url).then(
|
|
(res) => res.data?.analytics,
|
|
);
|
|
},
|
|
});
|
|
}
|