ieeeucsd-org/src/components/dashboard/ProfileSection/ShowProfileLogs.tsx
2025-04-26 15:22:55 -07:00

579 lines
25 KiB
TypeScript

import { useEffect, useState, useCallback, useMemo } from "react";
import { Authentication } from "../../../scripts/pocketbase/Authentication";
import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { Collections } from "../../../schemas/pocketbase/schema";
import debounce from 'lodash/debounce';
import type { Log } from "../../../schemas/pocketbase";
import { SendLog } from "../../../scripts/pocketbase/SendLog";
const LOGS_PER_PAGE = 5;
export default function ShowProfileLogs() {
const [logs, setLogs] = useState<Log[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalLogs, setTotalLogs] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
const [allLogs, setAllLogs] = useState<Log[]>([]);
const [isFetchingAll, setIsFetchingAll] = useState(false);
const [autoRefresh, setAutoRefresh] = useState(true);
// Auto-refresh logs every 30 seconds if enabled
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(() => {
if (!isFetchingAll) {
fetchLogs(true);
}
}, 30000); // 30 seconds
return () => clearInterval(interval);
}, [autoRefresh, isFetchingAll]);
const fetchLogs = async (skipCache = false) => {
setLoading(true);
setError(null);
const auth = Authentication.getInstance();
const currentUser = auth.getPocketBase().authStore.model;
const userId = currentUser?.id;
if (!userId) {
setError("Not authenticated");
setLoading(false);
return;
}
try {
setIsFetchingAll(true);
// console.log("Fetching logs for user:", userId);
// Use DataSyncService to fetch logs
const dataSync = DataSyncService.getInstance();
// First sync logs for this user
await dataSync.syncCollection(
Collections.LOGS,
`user = "${userId}"`,
"-created",
{ expand: "user" }
);
// Then get all logs from IndexedDB
const fetchedLogs = await dataSync.getData<Log>(
Collections.LOGS,
false, // Don't force sync again
`user = "${userId}"`,
"-created"
);
// console.log("Fetched logs:", fetchedLogs.length);
if (fetchedLogs.length === 0) {
// If no logs found, try to fetch directly from PocketBase
// console.log("No logs found in IndexedDB, trying direct fetch from PocketBase");
try {
const sendLog = SendLog.getInstance();
const directLogs = await sendLog.getUserLogs(userId);
// console.log("Direct fetch logs:", directLogs.length);
if (directLogs.length > 0) {
setAllLogs(directLogs);
setTotalPages(Math.ceil(directLogs.length / LOGS_PER_PAGE));
setTotalLogs(directLogs.length);
} else {
setAllLogs(fetchedLogs);
setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE));
setTotalLogs(fetchedLogs.length);
}
} catch (directError) {
// console.error("Failed to fetch logs directly:", directError);
setAllLogs(fetchedLogs);
setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE));
setTotalLogs(fetchedLogs.length);
}
} else {
setAllLogs(fetchedLogs);
setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE));
setTotalLogs(fetchedLogs.length);
}
} catch (error) {
// console.error("Failed to fetch logs:", error);
setError("Error loading activity");
} finally {
setLoading(false);
setIsFetchingAll(false);
}
};
// Memoized search function
const filteredLogs = useMemo(() => {
if (!allLogs.length) return [];
if (!searchQuery.trim()) {
// When not searching, return only the current page of logs
const startIndex = (currentPage - 1) * LOGS_PER_PAGE;
const endIndex = startIndex + LOGS_PER_PAGE;
return allLogs.slice(startIndex, endIndex);
}
const query = searchQuery.toLowerCase();
return allLogs.filter(log => {
return (
log.message?.toLowerCase().includes(query) ||
log.type?.toLowerCase().includes(query) ||
log.part?.toLowerCase().includes(query) ||
(log.created && new Date(log.created).toLocaleString().toLowerCase().includes(query))
);
});
}, [searchQuery, allLogs, currentPage]);
// Update displayed logs whenever filtered results change
useEffect(() => {
setLogs(filteredLogs);
// console.log("Filtered logs updated:", filteredLogs.length, "logs");
}, [filteredLogs]);
// Debounced search handler
const debouncedSearch = useCallback(
debounce((query: string) => {
setSearchQuery(query);
// Reset to first page when searching
if (query.trim()) {
setCurrentPage(1);
}
}, 300),
[]
);
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSearch(event.target.value);
};
const handlePrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const handleNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
const handleRefresh = () => {
fetchLogs(true);
};
useEffect(() => {
const loadLogsWithRetry = async () => {
try {
await fetchLogs();
// Wait a moment for state to update
setTimeout(async () => {
// Check if logs were loaded
if (allLogs.length === 0) {
// console.log("No logs found after initial fetch, trying direct fetch");
await directFetchLogs();
}
}, 1000);
} catch (error) {
// console.error("Failed to load logs with retry:", error);
}
};
loadLogsWithRetry();
checkLogsExist();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Check if there are any logs in the database at all
const checkLogsExist = async () => {
try {
const auth = Authentication.getInstance();
const pb = auth.getPocketBase();
// Check if the logs collection exists and has any records
const result = await pb.collection(Collections.LOGS).getList(1, 1);
// console.log("Logs collection check:", {
// totalItems: result.totalItems,
// page: result.page,
// perPage: result.perPage,
// totalPages: result.totalPages
// });
} catch (error) {
// console.error("Failed to check logs collection:", error);
}
};
// Calculate log statistics
const logStats = useMemo(() => {
if (!allLogs.length) return null;
const today = new Date();
today.setHours(0, 0, 0, 0);
const lastWeek = new Date(today);
lastWeek.setDate(lastWeek.getDate() - 7);
const todayLogs = allLogs.filter(log => new Date(log.created) >= today);
const weekLogs = allLogs.filter(log => new Date(log.created) >= lastWeek);
// Count by type
const typeCount: Record<string, number> = {};
allLogs.forEach(log => {
const type = log.type || 'unknown';
typeCount[type] = (typeCount[type] || 0) + 1;
});
return {
total: allLogs.length,
today: todayLogs.length,
week: weekLogs.length,
types: typeCount
};
}, [allLogs]);
// Direct fetch from PocketBase as a fallback
const directFetchLogs = async () => {
try {
setLoading(true);
setError(null);
const auth = Authentication.getInstance();
const pb = auth.getPocketBase();
const userId = auth.getPocketBase().authStore.model?.id;
if (!userId) {
setError("Not authenticated");
setLoading(false);
return;
}
// console.log("Direct fetching logs for user:", userId);
// Fetch logs directly from PocketBase
const result = await pb.collection(Collections.LOGS).getList<Log>(1, 100, {
filter: `user = "${userId}"`,
sort: "-created",
expand: "user"
});
// console.log("Direct fetch result:", {
// totalItems: result.totalItems,
// items: result.items.length
// });
if (result.items.length > 0) {
setAllLogs(result.items);
setTotalPages(Math.ceil(result.items.length / LOGS_PER_PAGE));
setTotalLogs(result.items.length);
}
} catch (error) {
// console.error("Failed to direct fetch logs:", error);
setError("Error loading activity");
} finally {
setLoading(false);
}
};
// Add a button to try direct fetch
const renderDirectFetchButton = () => (
<button
className="btn btn-sm btn-outline mt-4"
onClick={directFetchLogs}
>
Try Direct Fetch
</button>
);
if (loading && !allLogs.length) {
return (
<p className="text-base-content/70 flex items-center gap-2">
<svg className="h-5 w-5 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
{isFetchingAll ? 'Fetching your activity...' : 'Loading activity...'}
</p>
);
}
if (error) {
return (
<p className="text-base-content/70 flex items-center gap-2">
<svg className="h-5 w-5 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
{error}
</p>
);
}
// Debug logs
// console.log("Render state:", {
// logsLength: logs.length,
// allLogsLength: allLogs.length,
// searchQuery,
// loading,
// currentPage
// });
if (allLogs.length === 0 && !searchQuery && !loading) {
return (
<div>
<p className="text-base-content/70 flex items-center gap-2 mb-4">
<svg className="h-5 w-5 opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
No recent activity to display.
</p>
<div className="flex gap-2">
<button
className="btn btn-sm btn-primary"
onClick={async () => {
try {
const auth = Authentication.getInstance();
const userId = auth.getPocketBase().authStore.model?.id;
if (!userId) return;
const sendLog = SendLog.getInstance();
await sendLog.send(
"create",
"test",
"Test log created for debugging",
userId
);
// console.log("Created test log");
setTimeout(() => fetchLogs(true), 1000);
} catch (error) {
// console.error("Failed to create test log:", error);
}
}}
>
Create Test Log
</button>
<button
className="btn btn-sm btn-outline"
onClick={directFetchLogs}
>
Try Direct Fetch
</button>
</div>
</div>
);
}
return (
<div>
{/* Activity Summary */}
{logStats && !searchQuery && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="stat bg-base-200 rounded-lg p-4">
<div className="stat-title">Today</div>
<div className="stat-value">{logStats.today}</div>
<div className="stat-desc">Activities recorded today</div>
</div>
<div className="stat bg-base-200 rounded-lg p-4">
<div className="stat-title">This Week</div>
<div className="stat-value">{logStats.week}</div>
<div className="stat-desc">Activities in the last 7 days</div>
</div>
<div className="stat bg-base-200 rounded-lg p-4">
<div className="stat-title">Total</div>
<div className="stat-value">{logStats.total}</div>
<div className="stat-desc">All-time activities</div>
</div>
</div>
)}
{/* Search and Refresh Controls */}
<div className="flex gap-4 mb-4">
<div className="flex-1">
<input
type="text"
placeholder="Search activity..."
onChange={handleSearch}
className="input input-bordered w-full"
/>
</div>
<div className="dropdown dropdown-end dropdown-hover">
<button
onClick={handleRefresh}
className={`btn btn-ghost btn-square ${isFetchingAll ? 'loading' : ''}`}
title="Refresh logs"
disabled={isFetchingAll}
>
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M4.755 10.059a7.5 7.5 0 0112.548-3.364l1.903 1.903h-3.183a.75.75 0 100 1.5h4.992a.75.75 0 00.75-.75V4.356a.75.75 0 00-1.5 0v3.18l-1.9-1.9A9 9 0 003.306 9.67a.75.75 0 101.45.388zm15.408 3.352a.75.75 0 00-.919.53 7.5 7.5 0 01-12.548 3.364l-1.902-1.903h3.183a.75.75 0 000-1.5H2.984a.75.75 0 00-.75.75v4.992a.75.75 0 001.5 0v-3.18l1.9 1.9a9 9 0 0015.059-4.035.75.75 0 00-.53-.918z" clipRule="evenodd" />
</svg>
</button>
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className="flex justify-between"
>
<span>Auto-refresh</span>
<input
type="checkbox"
className="toggle toggle-primary toggle-sm"
checked={autoRefresh}
onChange={() => { }}
/>
</button>
</li>
<li><button onClick={directFetchLogs}>Direct fetch from server</button></li>
</ul>
</div>
</div>
{isFetchingAll && (
<div className="mb-4">
<p className="text-sm opacity-70">Fetching all activity, please wait...</p>
</div>
)}
{/* Search Results Message */}
{searchQuery && (
<p className="text-sm opacity-70 mb-4">
Found {logs.length} results for "{searchQuery}"
</p>
)}
{/* Logs Display */}
<div className="space-y-2">
{logs.map((log) => (
<div key={log.id} className="flex items-start gap-4 p-4 rounded-lg hover:bg-base-200 transition-colors duration-200">
<div className="shrink-0">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${getLogTypeColor(log.type)}`}>
{getLogTypeIcon(log.type)}
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-base font-medium">{log.message}</p>
<div className="flex items-center gap-2 mt-1">
{log.part && (
<span className="badge badge-sm">{log.part}</span>
)}
<p className="text-sm opacity-50">
{new Date(log.created).toLocaleString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
</div>
))}
</div>
{/* Pagination Controls */}
{!searchQuery && totalLogs > LOGS_PER_PAGE && (
<div className="flex justify-between items-center mt-6 pt-4 border-t border-base-200">
<div className="flex items-center gap-2">
<span className="text-sm opacity-70">
Showing {totalLogs ? (currentPage - 1) * LOGS_PER_PAGE + 1 : 0}-{Math.min(currentPage * LOGS_PER_PAGE, totalLogs)} of {totalLogs} results
</span>
</div>
<div className="flex gap-2">
<button
onClick={handlePrevPage}
disabled={currentPage === 1}
className="btn btn-sm btn-ghost"
>
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
Previous
</button>
<button
onClick={handleNextPage}
disabled={currentPage === totalPages}
className="btn btn-sm btn-ghost"
>
Next
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
</div>
)}
</div>
);
}
function getLogTypeColor(type: string): string {
switch (type?.toLowerCase()) {
case 'error':
return 'bg-error/10 text-error';
case 'update':
return 'bg-info/10 text-info';
case 'delete':
return 'bg-warning/10 text-warning';
case 'create':
return 'bg-success/10 text-success';
case 'login':
case 'logout':
return 'bg-primary/10 text-primary';
default:
return 'bg-base-300/50 text-base-content';
}
}
function getLogTypeIcon(type: string) {
switch (type?.toLowerCase()) {
case 'error':
return (
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
);
case 'update':
return (
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z" />
<path d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z" />
</svg>
);
case 'delete':
return (
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 013.878.512.75.75 0 11-.256 1.478l-.209-.035-1.005 13.07a3 3 0 01-2.991 2.77H8.084a3 3 0 01-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 01-.256-1.478A48.567 48.567 0 017.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 013.369 0c1.603.051 2.815 1.387 2.815 2.951zm-6.136-1.452a51.196 51.196 0 013.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 00-6 0v-.113c0-.794.609-1.428 1.364-1.452zm-.355 5.945a.75.75 0 10-1.5.058l.347 9a.75.75 0 101.499-.058l-.346-9zm5.48.058a.75.75 0 10-1.498-.058l-.347 9a.75.75 0 001.5.058l.345-9z" clipRule="evenodd" />
</svg>
);
case 'create':
return (
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" clipRule="evenodd" />
</svg>
);
case 'login':
return (
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M7.5 3.75A1.5 1.5 0 006 5.25v13.5a1.5 1.5 0 001.5 1.5h6a1.5 1.5 0 001.5-1.5V15a.75.75 0 011.5 0v3.75a3 3 0 01-3 3h-6a3 3 0 01-3-3V5.25a3 3 0 013-3h6a3 3 0 013 3V9A.75.75 0 0115 9V5.25a1.5 1.5 0 00-1.5-1.5h-6zm10.72 4.72a.75.75 0 011.06 0l3 3a.75.75 0 010 1.06l-3 3a.75.75 0 11-1.06-1.06l1.72-1.72H9a.75.75 0 010-1.5h10.94l-1.72-1.72a.75.75 0 010-1.06z" clipRule="evenodd" />
</svg>
);
case 'logout':
return (
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" d="M7.5 3.75A1.5 1.5 0 006 5.25v13.5a1.5 1.5 0 001.5 1.5h6a1.5 1.5 0 001.5-1.5V15a.75.75 0 011.5 0v3.75a3 3 0 01-3 3h-6a3 3 0 01-3-3V5.25a3 3 0 013-3h6a3 3 0 013 3V9A.75.75 0 0115 9V5.25a1.5 1.5 0 00-1.5-1.5h-6zm5.03 4.72a.75.75 0 010 1.06l-1.72 1.72h10.94a.75.75 0 010 1.5H10.81l1.72 1.72a.75.75 0 11-1.06 1.06l-3-3a.75.75 0 010-1.06l3-3a.75.75 0 011.06 0z" clipRule="evenodd" />
</svg>
);
default:
return (
<svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zm1-13h-2v6l5.25 3.15.75-1.23-4-2.37z" />
</svg>
);
}
}