Feature: Add collapsible login tables and customer activity report
- Make login activity tables collapsible (show/hide on demand) - Display only last 10 logins in collapsed tables - Add "Most Active Customers" report on customer login activity page - Shows top 10 most active customers with login counts - Fix back links on both login activity pages (was /admin, now /admin/orders) - Create CollapsibleLoginTable component for customer logins - Create CollapsibleAdminLoginTable component for admin logins Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
9623ac2fdb
commit
98b64ecd60
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface AdminLoginLog {
|
||||
id: string;
|
||||
loginAt: Date;
|
||||
success: boolean;
|
||||
ipAddress: string | null;
|
||||
userAgent: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
logs: AdminLoginLog[];
|
||||
}
|
||||
|
||||
export default function CollapsibleAdminLoginTable({ logs }: Props) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const recentLogs = logs.slice(0, 10);
|
||||
|
||||
const parseUserAgent = (ua: string | null) => {
|
||||
if (!ua) return { browser: 'Unknown', device: 'Unknown' };
|
||||
|
||||
let device = 'Unknown';
|
||||
if (ua.includes('Windows')) device = 'Windows';
|
||||
else if (ua.includes('Mac')) device = 'Mac';
|
||||
else if (ua.includes('Linux')) device = 'Linux';
|
||||
else if (ua.includes('iPhone')) device = 'iPhone';
|
||||
else if (ua.includes('Android')) device = 'Android';
|
||||
|
||||
let browser = 'Unknown';
|
||||
if (ua.includes('Chrome')) browser = 'Chrome';
|
||||
else if (ua.includes('Safari')) browser = 'Safari';
|
||||
else if (ua.includes('Firefox')) browser = 'Firefox';
|
||||
else if (ua.includes('Edge')) browser = 'Edge';
|
||||
|
||||
return { browser, device };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-line bg-surface rounded">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-surface-1 transition-colors"
|
||||
>
|
||||
<span className="font-medium">
|
||||
{isExpanded ? '▼' : '▶'} Last 10 Login Attempts
|
||||
</span>
|
||||
<span className="text-sm text-muted">{recentLogs.length} entries</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="border-t border-line overflow-x-auto">
|
||||
{recentLogs.length === 0 ? (
|
||||
<p className="p-4 text-sm text-muted">No login attempts recorded.</p>
|
||||
) : (
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-surface-1">
|
||||
<th className="text-left py-2 px-3 font-medium">Date & Time</th>
|
||||
<th className="text-left py-2 px-3 font-medium">Status</th>
|
||||
<th className="text-left py-2 px-3 font-medium">IP Address</th>
|
||||
<th className="text-left py-2 px-3 font-medium">Browser / Device</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentLogs.map((log) => {
|
||||
const { browser, device } = parseUserAgent(log.userAgent);
|
||||
return (
|
||||
<tr key={log.id} className="border-b border-line hover:bg-surface-1">
|
||||
<td className="py-2 px-3 font-mono text-xs">
|
||||
{new Date(log.loginAt).toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}{' '}
|
||||
{new Date(log.loginAt).toLocaleTimeString('en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<span
|
||||
className={`tag-label rounded-full px-3 py-1 text-xs ${
|
||||
log.success
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-splash-pink/10 text-splash-pink'
|
||||
}`}
|
||||
>
|
||||
{log.success ? 'Success' : 'Failed'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 font-mono text-xs">{log.ipAddress || '—'}</td>
|
||||
<td className="py-2 px-3 text-xs text-muted">
|
||||
{browser} / {device}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface LoginLog {
|
||||
id: string;
|
||||
email: string;
|
||||
loginAt: Date;
|
||||
success: boolean;
|
||||
ipAddress: string | null;
|
||||
userAgent: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
logs: LoginLog[];
|
||||
}
|
||||
|
||||
export default function CollapsibleLoginTable({ logs }: Props) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const recentLogs = logs.slice(0, 10);
|
||||
|
||||
const parseUserAgent = (ua: string | null) => {
|
||||
if (!ua) return { browser: 'Unknown', device: 'Unknown' };
|
||||
|
||||
let device = 'Unknown';
|
||||
if (ua.includes('Windows')) device = 'Windows';
|
||||
else if (ua.includes('Mac')) device = 'Mac';
|
||||
else if (ua.includes('Linux')) device = 'Linux';
|
||||
else if (ua.includes('iPhone')) device = 'iPhone';
|
||||
else if (ua.includes('Android')) device = 'Android';
|
||||
|
||||
let browser = 'Unknown';
|
||||
if (ua.includes('Chrome')) browser = 'Chrome';
|
||||
else if (ua.includes('Safari')) browser = 'Safari';
|
||||
else if (ua.includes('Firefox')) browser = 'Firefox';
|
||||
else if (ua.includes('Edge')) browser = 'Edge';
|
||||
|
||||
return { browser, device };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-line bg-surface rounded">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-surface-1 transition-colors"
|
||||
>
|
||||
<span className="font-medium">
|
||||
{isExpanded ? '▼' : '▶'} Last 10 Login Attempts
|
||||
</span>
|
||||
<span className="text-sm text-muted">{recentLogs.length} entries</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="border-t border-line overflow-x-auto">
|
||||
{recentLogs.length === 0 ? (
|
||||
<p className="p-4 text-sm text-muted">No login attempts recorded.</p>
|
||||
) : (
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-surface-1">
|
||||
<th className="text-left py-2 px-3 font-medium">Date & Time</th>
|
||||
<th className="text-left py-2 px-3 font-medium">Email</th>
|
||||
<th className="text-left py-2 px-3 font-medium">Status</th>
|
||||
<th className="text-left py-2 px-3 font-medium">IP Address</th>
|
||||
<th className="text-left py-2 px-3 font-medium">Browser / Device</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentLogs.map((log) => {
|
||||
const { browser, device } = parseUserAgent(log.userAgent);
|
||||
return (
|
||||
<tr key={log.id} className="border-b border-line hover:bg-surface-1">
|
||||
<td className="py-2 px-3 font-mono text-xs">
|
||||
{new Date(log.loginAt).toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}{' '}
|
||||
{new Date(log.loginAt).toLocaleTimeString('en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-xs">
|
||||
<span className="break-all">{log.email}</span>
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<span
|
||||
className={`tag-label rounded-full px-3 py-1 text-xs ${
|
||||
log.success
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-splash-pink/10 text-splash-pink'
|
||||
}`}
|
||||
>
|
||||
{log.success ? 'Success' : 'Failed'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 font-mono text-xs">{log.ipAddress || '—'}</td>
|
||||
<td className="py-2 px-3 text-xs text-muted">
|
||||
{browser} / {device}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user