performance improvments
This commit is contained in:
1 parent
912981e3f3
commit
02c9681aca
13 files changed
+805
-474
No files matched your search
@@ -9,9 +9,10 @@ import { formatDate } from "@/lib/utils";
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
function getTimeAgo(date: Date): string {
|
||||
function getTimeAgo(date: Date | string): string {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
const diffMs = now.getTime() - d.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
@@ -22,7 +23,7 @@ function getTimeAgo(date: Date): string {
|
||||
return `${diffDays}d ago`;
|
||||
}
|
||||
|
||||
interface User {
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
@@ -31,15 +32,18 @@ interface User {
|
||||
phone?: string;
|
||||
gymId?: string;
|
||||
gymName?: string | null;
|
||||
createdAt: Date;
|
||||
createdAt?: string | Date;
|
||||
isCheckedIn?: boolean;
|
||||
checkInTime?: Date;
|
||||
checkInTime?: string | Date;
|
||||
lastCheckInTime?: string | Date;
|
||||
checkInsThisWeek?: number;
|
||||
checkInsThisMonth?: number;
|
||||
client?: {
|
||||
id: string;
|
||||
membershipType: string;
|
||||
membershipStatus: string;
|
||||
joinDate: Date;
|
||||
lastVisit?: Date;
|
||||
joinDate: string | Date;
|
||||
lastVisit?: string | Date;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { UserGrid } from "@/components/users/UserGrid";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { UserGrid, type User } from "@/components/users/UserGrid";
|
||||
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import { getGymIdFromUser } from "@/lib/error-helpers";
|
||||
import log from "@/lib/logger";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { CreateUserModal } from "./CreateUserModal";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
role: string;
|
||||
phone?: string;
|
||||
gymId?: string;
|
||||
createdAt: Date;
|
||||
isCheckedIn?: boolean;
|
||||
checkInTime?: Date;
|
||||
lastCheckInTime?: Date;
|
||||
checkInsThisWeek?: number;
|
||||
checkInsThisMonth?: number;
|
||||
client?: {
|
||||
id: string;
|
||||
membershipType: string;
|
||||
membershipStatus: string;
|
||||
joinDate: Date;
|
||||
lastVisit?: Date;
|
||||
};
|
||||
}
|
||||
import {
|
||||
useUsers,
|
||||
useGyms,
|
||||
useUpdateUser,
|
||||
useDeleteUser,
|
||||
useSendInvitation,
|
||||
} from "@/hooks/use-api";
|
||||
|
||||
export function UserManagement() {
|
||||
const { user } = useUser();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState<string>("all");
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -52,68 +32,17 @@ export function UserManagement() {
|
||||
gymId: string;
|
||||
} | null>(null);
|
||||
|
||||
// Active gyms for dropdown
|
||||
const [gyms, setGyms] = useState<Array<{ id: string; name: string }>>([]);
|
||||
|
||||
// Load gyms when modal opens or refreshes
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/gyms");
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
// map down to id and name to avoid extra payload use here
|
||||
setGyms(data.map((g: any) => ({ id: g.id, name: g.name })));
|
||||
} else {
|
||||
setGyms([]);
|
||||
}
|
||||
} catch {
|
||||
setGyms([]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [filter]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const ts = Date.now();
|
||||
const url =
|
||||
filter === "all"
|
||||
? `/api/users?ts=${ts}`
|
||||
: `/api/users?role=${filter}&ts=${ts}`;
|
||||
|
||||
log.debug("Fetching users", { url });
|
||||
const response = await fetch(url, { cache: "no-store" });
|
||||
log.debug("Users fetch response", {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
});
|
||||
const responseData = await response.json();
|
||||
const data = responseData.data || responseData; // Handle both old and new API response formats
|
||||
log.debug("Received users data", {
|
||||
count: Array.isArray(data.users) ? data.users.length : 0,
|
||||
sample:
|
||||
data.users && data.users[0]
|
||||
? {
|
||||
id: data.users[0].id,
|
||||
gymId: data.users[0].gymId,
|
||||
role: data.users[0].role,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
setUsers(data.users || []);
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch users", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const {
|
||||
data: users = [],
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useUsers({
|
||||
role: filter !== "all" ? filter : undefined,
|
||||
});
|
||||
const { data: gyms = [] } = useGyms();
|
||||
const updateUser = useUpdateUser();
|
||||
const deleteUser = useDeleteUser();
|
||||
const sendInvitation = useSendInvitation();
|
||||
|
||||
const handleUserSelect = (user: User | null) => {
|
||||
setSelectedUser(user);
|
||||
@@ -144,17 +73,12 @@ export function UserManagement() {
|
||||
return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/users", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ids: users.map((u) => u.id) }),
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchUsers();
|
||||
toast.success("Users deleted successfully");
|
||||
} else {
|
||||
toast.error("Error deleting users");
|
||||
}
|
||||
const deletePromises = users.map((u) =>
|
||||
fetch(`/api/users?id=${u.id}`, { method: "DELETE" }),
|
||||
);
|
||||
await Promise.all(deletePromises);
|
||||
refetch();
|
||||
toast.success("Users deleted successfully");
|
||||
} catch (error) {
|
||||
log.error("Failed to delete users", error);
|
||||
}
|
||||
@@ -196,7 +120,7 @@ export function UserManagement() {
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchUsers();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
@@ -204,7 +128,6 @@ export function UserManagement() {
|
||||
|
||||
try {
|
||||
if (selectedUser) {
|
||||
// Update existing user
|
||||
const payload = {
|
||||
id: selectedUser.id,
|
||||
email: editForm.email,
|
||||
@@ -214,80 +137,21 @@ export function UserManagement() {
|
||||
phone: editForm.phone,
|
||||
gymId: editForm.gymId === "" ? null : editForm.gymId,
|
||||
};
|
||||
log.debug("Updating user", payload);
|
||||
const response = await fetch("/api/users", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
log.debug("User update response", {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
});
|
||||
if (response.ok) {
|
||||
// Optimistically update local state so grid reflects changes immediately
|
||||
setUsers((prev) =>
|
||||
prev.map((u) =>
|
||||
u.id === selectedUser.id
|
||||
? {
|
||||
...u,
|
||||
email: editForm.email,
|
||||
firstName: editForm.firstName,
|
||||
lastName: editForm.lastName,
|
||||
role: editForm.role,
|
||||
phone: editForm.phone || undefined,
|
||||
gymId: editForm.gymId === "" ? undefined : editForm.gymId,
|
||||
}
|
||||
: u,
|
||||
),
|
||||
);
|
||||
setSelectedUser((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
email: editForm.email,
|
||||
firstName: editForm.firstName,
|
||||
lastName: editForm.lastName,
|
||||
role: editForm.role,
|
||||
phone: editForm.phone || undefined,
|
||||
gymId: editForm.gymId === "" ? undefined : editForm.gymId,
|
||||
}
|
||||
: prev,
|
||||
);
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
// Still re-fetch from server to ensure consistency
|
||||
log.debug("Re-fetching users after successful edit");
|
||||
fetchUsers();
|
||||
toast.success("User updated successfully");
|
||||
} else {
|
||||
const errText = await response.text().catch(() => "");
|
||||
log.error("User update failed", new Error(errText), {
|
||||
status: response.status,
|
||||
});
|
||||
toast.error("Error updating user");
|
||||
}
|
||||
} else {
|
||||
// Create (Invite) new user
|
||||
const response = await fetch("/api/invitations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
inviteeEmail: editForm.email,
|
||||
roleAssigned: editForm.role,
|
||||
gymId: editForm.gymId || undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
fetchUsers();
|
||||
toast.success("Invitation sent successfully!");
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
toast.error(`Error sending invitation: ${errorData.error}`);
|
||||
}
|
||||
await updateUser.mutateAsync(payload);
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
refetch();
|
||||
toast.success("User updated successfully");
|
||||
} else {
|
||||
await sendInvitation.mutateAsync({
|
||||
email: editForm.email,
|
||||
role: editForm.role,
|
||||
});
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
refetch();
|
||||
toast.success("Invitation sent successfully!");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -299,17 +163,11 @@ export function UserManagement() {
|
||||
if (!selectedUser) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users?id=${selectedUser.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.ok) {
|
||||
setIsDeleting(false);
|
||||
setSelectedUser(null);
|
||||
fetchUsers();
|
||||
toast.success("User deleted successfully");
|
||||
} else {
|
||||
toast.error("Error deleting user");
|
||||
}
|
||||
await deleteUser.mutateAsync(selectedUser.id);
|
||||
setIsDeleting(false);
|
||||
setSelectedUser(null);
|
||||
refetch();
|
||||
toast.success("User deleted successfully");
|
||||
} catch (error) {
|
||||
log.error("Failed to delete user", error);
|
||||
}
|
||||
@@ -397,7 +255,7 @@ export function UserManagement() {
|
||||
onEditUser={handleEditUser}
|
||||
onDeleteUser={handleDeleteUser}
|
||||
onBulkDelete={handleBulkDelete}
|
||||
loading={loading}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -591,7 +449,9 @@ export function UserManagement() {
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Joined:</span>{" "}
|
||||
{new Date(selectedUser.createdAt).toLocaleDateString()}
|
||||
{selectedUser.createdAt
|
||||
? new Date(selectedUser.createdAt).toLocaleDateString()
|
||||
: "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -653,7 +513,7 @@ export function UserManagement() {
|
||||
<CreateUserModal
|
||||
open={createModalOpen}
|
||||
onOpenChange={setCreateModalOpen}
|
||||
onSuccess={() => fetchUsers()}
|
||||
onSuccess={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user