Files
fitaiProto/apps/admin/src/components/users/UserGrid.tsx
T
dimitar 8353a24f6b cleanup
and AI recommnedation fix
2025-12-12 01:32:52 +01:00

310 lines
9.1 KiB
TypeScript

"use client";
import React, { useState, useMemo } from "react";
import { AgGridReact } from "ag-grid-react";
import { ColDef, ModuleRegistry, AllCommunityModule } from "ag-grid-community";
import "ag-grid-community/styles/ag-grid.css";
import "ag-grid-community/styles/ag-theme-alpine.css";
import { formatDate } from "@/lib/utils";
ModuleRegistry.registerModules([AllCommunityModule]);
function getTimeAgo(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
return `${diffDays}d ago`;
}
interface User {
id: string;
email: string;
firstName: string;
lastName: string;
role: string;
phone?: string;
createdAt: Date;
isCheckedIn?: boolean;
checkInTime?: Date;
client?: {
id: string;
membershipType: string;
membershipStatus: string;
joinDate: Date;
lastVisit?: Date;
};
}
interface UserGridProps {
users: User[];
onUserSelect?: (user: User) => void;
onEditUser?: (user: User) => void;
onDeleteUser?: (user: User) => void;
onBulkDelete?: (users: User[]) => void;
loading?: boolean;
}
export function UserGrid({
users,
onUserSelect,
onEditUser,
onDeleteUser,
onBulkDelete,
loading = false,
}: UserGridProps) {
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const [searchQuery, setSearchQuery] = useState<string>("");
const columnDefs: ColDef<User>[] = useMemo(
() => [
{
headerName: "Name",
valueGetter: (params) =>
`${params.data?.firstName} ${params.data?.lastName}`,
filter: "agTextColumnFilter",
sortable: true,
minWidth: 150,
},
{
headerName: "Email",
field: "email",
filter: "agTextColumnFilter",
sortable: true,
minWidth: 200,
},
{
headerName: "Role",
field: "role",
filter: "agTextColumnFilter",
sortable: true,
cellRenderer: (params: any) => {
const roleColors = {
superAdmin: "bg-red-100 text-red-800",
admin: "bg-purple-100 text-purple-800",
trainer: "bg-blue-100 text-blue-800",
client: "bg-green-100 text-green-800",
};
const colorClass =
roleColors[params.value as keyof typeof roleColors] ||
"bg-gray-100 text-gray-800";
const label = params.value === 'superAdmin' ? 'Super Admin' : params.value.charAt(0).toUpperCase() + params.value.slice(1);
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
{label}
</span>
);
},
minWidth: 120,
},
{
headerName: "Phone",
field: "phone",
filter: "agTextColumnFilter",
sortable: true,
minWidth: 130,
valueFormatter: (params: any) => params.value || "N/A",
},
{
headerName: "Membership",
valueGetter: (params) => params.data?.client?.membershipType || "N/A",
filter: "agTextColumnFilter",
sortable: true,
cellRenderer: (params: any) => {
if (!params.value || params.value === "N/A") return <span className="text-gray-400">N/A</span>;
const membershipColors = {
vip: "bg-yellow-100 text-yellow-800 border-yellow-200",
premium: "bg-blue-100 text-blue-800 border-blue-200",
basic: "bg-slate-100 text-slate-800 border-slate-200",
};
const colorClass =
membershipColors[params.value as keyof typeof membershipColors] ||
"bg-gray-100 text-gray-800";
const label = params.value.charAt(0).toUpperCase() + params.value.slice(1);
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium border ${colorClass}`}>
{label}
</span>
);
},
minWidth: 120,
},
{
headerName: "Status",
valueGetter: (params) => params.data?.client?.membershipStatus || "N/A",
filter: "agTextColumnFilter",
sortable: true,
cellRenderer: (params: any) => {
if (!params.value || params.value === "N/A") return <span className="text-gray-400">N/A</span>;
const statusColors = {
active: "bg-green-100 text-green-800",
inactive: "bg-red-100 text-red-800",
suspended: "bg-orange-100 text-orange-800",
expired: "bg-gray-100 text-gray-800",
};
const colorClass =
statusColors[params.value as keyof typeof statusColors] ||
"bg-gray-100 text-gray-800";
const label = params.value.charAt(0).toUpperCase() + params.value.slice(1);
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
{label}
</span>
);
},
minWidth: 120,
},
{
headerName: "Currently Checked In",
valueGetter: (params) => params.data?.isCheckedIn,
filter: "agTextColumnFilter",
sortable: true,
cellRenderer: (params: any) => {
if (!params.data?.isCheckedIn) {
return <span className="text-gray-400">—</span>;
}
const checkInTime = params.data.checkInTime ? new Date(params.data.checkInTime) : null;
const timeAgo = checkInTime ? getTimeAgo(checkInTime) : '';
return (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 border border-green-200">
✓ Checked In {timeAgo && `(${timeAgo})`}
</span>
);
},
minWidth: 180,
},
{
headerName: "Join Date",
valueGetter: (params) =>
params.data?.client?.joinDate || params.data?.createdAt,
filter: "agDateColumnFilter",
sortable: true,
valueFormatter: (params: any) => formatDate(new Date(params.value)),
minWidth: 120,
},
{
headerName: "Last Visit",
valueGetter: (params) => params.data?.client?.lastVisit,
filter: "agDateColumnFilter",
sortable: true,
valueFormatter: (params: any) =>
params.value ? formatDate(new Date(params.value)) : "Never",
minWidth: 120,
},
],
[],
);
const defaultColDef: ColDef = useMemo(
() => ({
flex: 1,
resizable: true,
floatingFilter: true,
suppressMenu: true,
}),
[],
);
const gridRef = React.useRef<AgGridReact<User>>(null);
const gridOptions = {
theme: "legacy" as const,
columnDefs,
defaultColDef,
rowData: users,
rowSelection: "multiple" as const,
onSelectionChanged: () => {
const selectedNodes = gridRef.current?.api.getSelectedNodes();
const selectedData = selectedNodes?.map((node) => node.data).filter((u): u is User => !!u) || [];
setSelectedUsers(selectedData);
if (selectedData.length === 1 && onUserSelect) {
onUserSelect(selectedData[0]);
}
},
suppressRowClickSelection: false,
animateRows: true,
loading: loading,
pagination: true,
paginationPageSize: 20,
paginationPageSizeSelector: [10, 20, 50, 100],
quickFilterText: searchQuery,
};
const handleEdit = () => {
if (selectedUsers.length === 1 && onEditUser) {
onEditUser(selectedUsers[0]);
}
};
const handleDelete = () => {
if (selectedUsers.length === 1 && onDeleteUser) {
onDeleteUser(selectedUsers[0]);
}
};
const handleBulkDelete = () => {
if (selectedUsers.length > 0 && onBulkDelete) {
onBulkDelete(selectedUsers);
}
};
return (
<div>
<div className="flex justify-between items-center mb-4">
<input
type="text"
placeholder="Search users..."
className="border border-gray-300 rounded px-4 py-2"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<div className="flex gap-2">
<button
className="bg-green-500 text-white px-4 py-2 rounded disabled:opacity-50"
onClick={handleEdit}
disabled={selectedUsers.length !== 1}
>
Edit User
</button>
<button
className="bg-red-500 text-white px-4 py-2 rounded disabled:opacity-50"
onClick={handleDelete}
disabled={selectedUsers.length !== 1}
>
Delete User
</button>
<button
className="bg-yellow-500 text-white px-4 py-2 rounded disabled:opacity-50"
onClick={handleBulkDelete}
disabled={selectedUsers.length === 0}
>
Bulk Delete
</button>
</div>
</div>
<div
className="ag-theme-alpine"
style={{ height: "600px", width: "100%" }}
>
<AgGridReact<User> {...gridOptions} ref={gridRef} />
</div>
</div>
);
}