538 lines
17 KiB
TypeScript
538 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { UserGrid } from "@/components/users/UserGrid";
|
|
// import { Button } from "@/components/ui/button";
|
|
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
role: string;
|
|
phone?: string;
|
|
createdAt: Date;
|
|
isCheckedIn?: boolean;
|
|
checkInTime?: Date;
|
|
lastCheckInTime?: Date;
|
|
checkInsThisWeek?: number;
|
|
checkInsThisMonth?: number;
|
|
client?: {
|
|
id: string;
|
|
membershipType: string;
|
|
membershipStatus: string;
|
|
joinDate: Date;
|
|
lastVisit?: Date;
|
|
};
|
|
}
|
|
|
|
export function UserManagement() {
|
|
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);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [editForm, setEditForm] = useState<{
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
role: string;
|
|
phone: string;
|
|
} | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
}, [filter]);
|
|
|
|
const fetchUsers = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const url = filter === "all" ? "/api/users" : `/api/users?role=${filter}`;
|
|
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
setUsers(data.users || []);
|
|
} catch (error) {
|
|
console.error("Failed to fetch users:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleUserSelect = (user: User | null) => {
|
|
setSelectedUser(user);
|
|
};
|
|
|
|
const handleEditUser = (user: User) => {
|
|
setSelectedUser(user);
|
|
setEditForm({
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
email: user.email,
|
|
role: user.role,
|
|
phone: user.phone || "",
|
|
});
|
|
setIsEditing(true);
|
|
};
|
|
|
|
const handleDeleteUser = (user: User) => {
|
|
setSelectedUser(user);
|
|
setIsDeleting(true);
|
|
};
|
|
|
|
const handleBulkDelete = async (users: User[]) => {
|
|
if (users.length === 0) return;
|
|
|
|
if (!confirm(`Are you sure you want to delete ${users.length} users?`))
|
|
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();
|
|
} else {
|
|
alert("Error deleting users");
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const handleExport = () => {
|
|
const csvContent = [
|
|
[
|
|
"Name",
|
|
"Email",
|
|
"Role",
|
|
"Phone",
|
|
"Membership",
|
|
"Status",
|
|
"Join Date",
|
|
"Last Visit",
|
|
],
|
|
...users.map((user) => [
|
|
`${user.firstName} ${user.lastName}`,
|
|
user.email,
|
|
user.role,
|
|
user.phone || "",
|
|
user.client?.membershipType || "",
|
|
user.client?.membershipStatus || "",
|
|
user.client?.joinDate || user.createdAt,
|
|
user.client?.lastVisit || "",
|
|
]),
|
|
]
|
|
.map((row) => row.join(","))
|
|
.join("\n");
|
|
|
|
const blob = new Blob([csvContent], { type: "text/csv" });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `users_${new Date().toISOString().split("T")[0]}.csv`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const handleRefresh = () => {
|
|
fetchUsers();
|
|
};
|
|
|
|
const handleSaveEdit = async () => {
|
|
if (!editForm) return;
|
|
|
|
try {
|
|
if (selectedUser) {
|
|
// Update existing user
|
|
const response = await fetch("/api/users", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ id: selectedUser.id, ...editForm }),
|
|
});
|
|
if (response.ok) {
|
|
setIsEditing(false);
|
|
setEditForm(null);
|
|
fetchUsers();
|
|
} else {
|
|
alert("Error updating user");
|
|
}
|
|
} else {
|
|
// Create (Invite) new user
|
|
const response = await fetch("/api/users", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(editForm),
|
|
});
|
|
|
|
if (response.ok) {
|
|
setIsEditing(false);
|
|
setEditForm(null);
|
|
fetchUsers();
|
|
alert("Invitation sent successfully!");
|
|
} else {
|
|
const errorData = await response.json();
|
|
alert(`Error sending invitation: ${errorData.error}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert("An unexpected error occurred");
|
|
}
|
|
};
|
|
|
|
const handleDeleteConfirm = async () => {
|
|
if (!selectedUser) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/users?id=${selectedUser.id}`, {
|
|
method: "DELETE",
|
|
});
|
|
if (response.ok) {
|
|
setIsDeleting(false);
|
|
setSelectedUser(null);
|
|
fetchUsers();
|
|
} else {
|
|
alert("Error deleting user");
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-2xl font-bold">User Management</h2>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant={filter === "all" ? "default" : "outline"}
|
|
onClick={() => setFilter("all")}
|
|
>
|
|
All Users
|
|
</Button>
|
|
<Button
|
|
variant={filter === "client" ? "default" : "outline"}
|
|
onClick={() => selectedUser && handleEditUser(selectedUser)}
|
|
disabled={!selectedUser}
|
|
>
|
|
Edit User
|
|
</Button>
|
|
<Button
|
|
variant={filter === "client" ? "default" : "outline"}
|
|
onClick={() => {
|
|
setEditForm({
|
|
firstName: "",
|
|
lastName: "",
|
|
email: "",
|
|
role: "client",
|
|
phone: "",
|
|
});
|
|
setSelectedUser(null);
|
|
setIsEditing(true);
|
|
}}
|
|
>
|
|
Invite User
|
|
</Button>
|
|
<Button
|
|
variant={filter === "client" ? "default" : "outline"}
|
|
onClick={() => selectedUser && handleDeleteUser(selectedUser)}
|
|
disabled={!selectedUser}
|
|
>
|
|
Delete User
|
|
</Button>
|
|
<Button
|
|
variant={filter === "client" ? "default" : "outline"}
|
|
onClick={() => setFilter("client")}
|
|
>
|
|
Clientsa
|
|
</Button>
|
|
<Button
|
|
variant={filter === "trainer" ? "default" : "outline"}
|
|
onClick={() => setFilter("trainer")}
|
|
>
|
|
Trainers
|
|
</Button>
|
|
<Button
|
|
variant={filter === "admin" ? "default" : "outline"}
|
|
onClick={() => setFilter("admin")}
|
|
>
|
|
Admins
|
|
</Button>
|
|
<Button
|
|
variant={filter === "superAdmin" ? "default" : "outline"}
|
|
onClick={() => setFilter("superAdmin")}
|
|
>
|
|
Super Admins
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center">
|
|
<div className="text-sm text-gray-600">
|
|
Showing {users.length} users
|
|
{selectedUser && (
|
|
<span className="ml-4 text-blue-600">
|
|
Selected: {selectedUser.firstName} {selectedUser.lastName}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="default" onClick={handleRefresh}>
|
|
Refresh
|
|
</Button>
|
|
<Button variant="default" onClick={handleExport}>
|
|
Export CSV
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<UserGrid
|
|
users={users}
|
|
onUserSelect={(user) => handleUserSelect(user)}
|
|
onEditUser={handleEditUser}
|
|
onDeleteUser={handleDeleteUser}
|
|
onBulkDelete={handleBulkDelete}
|
|
loading={loading}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{isEditing && editForm && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full">
|
|
<h3 className="text-lg font-semibold mb-4">{selectedUser ? 'Edit User' : 'Invite New User'}</h3>
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
handleSaveEdit();
|
|
}}
|
|
>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium mb-1">
|
|
First Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={editForm.firstName}
|
|
onChange={(e) =>
|
|
setEditForm({ ...editForm, firstName: e.target.value })
|
|
}
|
|
className="w-full border border-gray-300 rounded px-3 py-2"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium mb-1">
|
|
Last Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={editForm.lastName}
|
|
onChange={(e) =>
|
|
setEditForm({ ...editForm, lastName: e.target.value })
|
|
}
|
|
className="w-full border border-gray-300 rounded px-3 py-2"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium mb-1">Email</label>
|
|
<input
|
|
type="email"
|
|
value={editForm.email}
|
|
onChange={(e) =>
|
|
setEditForm({ ...editForm, email: e.target.value })
|
|
}
|
|
className="w-full border border-gray-300 rounded px-3 py-2"
|
|
required
|
|
disabled={!!selectedUser} // Disable email edit for existing users if desired, or keep enabled
|
|
/>
|
|
</div>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium mb-1">Role</label>
|
|
<select
|
|
value={editForm.role}
|
|
onChange={(e) =>
|
|
setEditForm({ ...editForm, role: e.target.value })
|
|
}
|
|
className="w-full border border-gray-300 rounded px-3 py-2"
|
|
required
|
|
>
|
|
{/* Ideally we fetch current user role to filter these.
|
|
For now, we show all but the API will enforce it.
|
|
We can add a visual indicator or fetch "me" to filter. */}
|
|
<option value="client">Client</option>
|
|
<option value="trainer">Trainer</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="superAdmin">Super Admin</option>
|
|
</select>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Note: You can only assign roles lower than your own.
|
|
</p>
|
|
</div>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium mb-1">Phone</label>
|
|
<input
|
|
type="tel"
|
|
value={editForm.phone}
|
|
onChange={(e) =>
|
|
setEditForm({ ...editForm, phone: e.target.value })
|
|
}
|
|
className="w-full border border-gray-300 rounded px-3 py-2"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setIsEditing(false);
|
|
setEditForm(null);
|
|
}}
|
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
>
|
|
{selectedUser ? 'Save Changes' : 'Send Invitation'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isDeleting && selectedUser && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full">
|
|
<h3 className="text-lg font-semibold mb-4">Delete User</h3>
|
|
<p className="mb-4">
|
|
Are you sure you want to delete {selectedUser.firstName}{" "}
|
|
{selectedUser.lastName}? This action cannot be undone.
|
|
</p>
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
onClick={() => setIsDeleting(false)}
|
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleDeleteConfirm}
|
|
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedUser && (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<h3 className="text-lg font-semibold">User Details</h3>
|
|
<a
|
|
href={`/users/${selectedUser.id}`}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm font-medium"
|
|
>
|
|
View Full Profile & Recommendations
|
|
</a>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<h4 className="font-medium mb-2">Basic Information</h4>
|
|
<div className="space-y-1 text-sm">
|
|
<p>
|
|
<span className="font-medium">Name:</span>{" "}
|
|
{selectedUser.firstName} {selectedUser.lastName}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">Email:</span>{" "}
|
|
{selectedUser.email}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">Phone:</span>{" "}
|
|
{selectedUser.phone || "N/A"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">Role:</span>{" "}
|
|
{selectedUser.role}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">Joined:</span>{" "}
|
|
{new Date(selectedUser.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedUser.client && (
|
|
<div>
|
|
<h4 className="font-medium mb-2">Client Information</h4>
|
|
<div className="space-y-1 text-sm">
|
|
<p>
|
|
<span className="font-medium">Membership:</span>{" "}
|
|
{selectedUser.client.membershipType}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">Status:</span>{" "}
|
|
{selectedUser.client.membershipStatus}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">Member Since:</span>{" "}
|
|
{new Date(
|
|
selectedUser.client.joinDate,
|
|
).toLocaleDateString()}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">Last Visit:</span>{" "}
|
|
{selectedUser.client.lastVisit
|
|
? new Date(
|
|
selectedUser.client.lastVisit,
|
|
).toLocaleDateString()
|
|
: "Never"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<h4 className="font-medium mb-2">Check-In Statistics</h4>
|
|
<div className="space-y-1 text-sm">
|
|
<p>
|
|
<span className="font-medium">Last Check-In:</span>{" "}
|
|
{selectedUser.lastCheckInTime
|
|
? new Date(
|
|
selectedUser.lastCheckInTime,
|
|
).toLocaleString()
|
|
: "Never"}
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">This Week:</span>{" "}
|
|
{selectedUser.checkInsThisWeek || 0} check-ins
|
|
</p>
|
|
<p>
|
|
<span className="font-medium">This Month:</span>{" "}
|
|
{selectedUser.checkInsThisMonth || 0} check-ins
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|