428 lines
12 KiB
TypeScript
428 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useAuth } from "../auth-context";
|
|
import { API_URL } from "../config";
|
|
|
|
interface CounterRecord {
|
|
id: number;
|
|
user_email: string;
|
|
value_before: number;
|
|
value_after: number;
|
|
created_at: string;
|
|
}
|
|
|
|
interface SumRecord {
|
|
id: number;
|
|
user_email: string;
|
|
a: number;
|
|
b: number;
|
|
result: number;
|
|
created_at: string;
|
|
}
|
|
|
|
interface PaginatedResponse<T> {
|
|
records: T[];
|
|
total: number;
|
|
page: number;
|
|
per_page: number;
|
|
total_pages: number;
|
|
}
|
|
|
|
export default function AuditPage() {
|
|
const [counterData, setCounterData] = useState<PaginatedResponse<CounterRecord> | null>(null);
|
|
const [sumData, setSumData] = useState<PaginatedResponse<SumRecord> | null>(null);
|
|
const [counterPage, setCounterPage] = useState(1);
|
|
const [sumPage, setSumPage] = useState(1);
|
|
const { user, isLoading, logout } = useAuth();
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && !user) {
|
|
router.push("/login");
|
|
}
|
|
}, [isLoading, user, router]);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchCounterRecords(counterPage);
|
|
}
|
|
}, [user, counterPage]);
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchSumRecords(sumPage);
|
|
}
|
|
}, [user, sumPage]);
|
|
|
|
const fetchCounterRecords = async (page: number) => {
|
|
try {
|
|
const res = await fetch(`${API_URL}/api/audit/counter?page=${page}&per_page=10`, {
|
|
credentials: "include",
|
|
});
|
|
const data = await res.json();
|
|
setCounterData(data);
|
|
} catch {
|
|
setCounterData(null);
|
|
}
|
|
};
|
|
|
|
const fetchSumRecords = async (page: number) => {
|
|
try {
|
|
const res = await fetch(`${API_URL}/api/audit/sum?page=${page}&per_page=10`, {
|
|
credentials: "include",
|
|
});
|
|
const data = await res.json();
|
|
setSumData(data);
|
|
} catch {
|
|
setSumData(null);
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
router.push("/login");
|
|
};
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
return new Date(dateStr).toLocaleString();
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<main style={styles.main}>
|
|
<div style={styles.loader}>Loading...</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<main style={styles.main}>
|
|
<div style={styles.header}>
|
|
<div style={styles.nav}>
|
|
<a href="/" style={styles.navLink}>Counter</a>
|
|
<span style={styles.navDivider}>•</span>
|
|
<a href="/sum" style={styles.navLink}>Sum</a>
|
|
<span style={styles.navDivider}>•</span>
|
|
<span style={styles.navCurrent}>Audit</span>
|
|
</div>
|
|
<div style={styles.userInfo}>
|
|
<span style={styles.userEmail}>{user.email}</span>
|
|
<button onClick={handleLogout} style={styles.logoutBtn}>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={styles.content}>
|
|
<div style={styles.tablesContainer}>
|
|
{/* Counter Records Table */}
|
|
<div style={styles.tableCard}>
|
|
<div style={styles.tableHeader}>
|
|
<h2 style={styles.tableTitle}>Counter Activity</h2>
|
|
<span style={styles.totalCount}>
|
|
{counterData?.total ?? 0} records
|
|
</span>
|
|
</div>
|
|
<div style={styles.tableWrapper}>
|
|
<table style={styles.table}>
|
|
<thead>
|
|
<tr>
|
|
<th style={styles.th}>User</th>
|
|
<th style={styles.th}>Before</th>
|
|
<th style={styles.th}>After</th>
|
|
<th style={styles.th}>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{counterData?.records.map((record) => (
|
|
<tr key={record.id} style={styles.tr}>
|
|
<td style={styles.td}>{record.user_email}</td>
|
|
<td style={styles.tdNum}>{record.value_before}</td>
|
|
<td style={styles.tdNum}>{record.value_after}</td>
|
|
<td style={styles.tdDate}>{formatDate(record.created_at)}</td>
|
|
</tr>
|
|
))}
|
|
{(!counterData || counterData.records.length === 0) && (
|
|
<tr>
|
|
<td colSpan={4} style={styles.emptyRow}>No records yet</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{counterData && counterData.total_pages > 1 && (
|
|
<div style={styles.pagination}>
|
|
<button
|
|
onClick={() => setCounterPage((p) => Math.max(1, p - 1))}
|
|
disabled={counterPage === 1}
|
|
style={styles.pageBtn}
|
|
>
|
|
←
|
|
</button>
|
|
<span style={styles.pageInfo}>
|
|
{counterPage} / {counterData.total_pages}
|
|
</span>
|
|
<button
|
|
onClick={() => setCounterPage((p) => Math.min(counterData.total_pages, p + 1))}
|
|
disabled={counterPage === counterData.total_pages}
|
|
style={styles.pageBtn}
|
|
>
|
|
→
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sum Records Table */}
|
|
<div style={styles.tableCard}>
|
|
<div style={styles.tableHeader}>
|
|
<h2 style={styles.tableTitle}>Sum Activity</h2>
|
|
<span style={styles.totalCount}>
|
|
{sumData?.total ?? 0} records
|
|
</span>
|
|
</div>
|
|
<div style={styles.tableWrapper}>
|
|
<table style={styles.table}>
|
|
<thead>
|
|
<tr>
|
|
<th style={styles.th}>User</th>
|
|
<th style={styles.th}>A</th>
|
|
<th style={styles.th}>B</th>
|
|
<th style={styles.th}>Result</th>
|
|
<th style={styles.th}>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sumData?.records.map((record) => (
|
|
<tr key={record.id} style={styles.tr}>
|
|
<td style={styles.td}>{record.user_email}</td>
|
|
<td style={styles.tdNum}>{record.a}</td>
|
|
<td style={styles.tdNum}>{record.b}</td>
|
|
<td style={styles.tdResult}>{record.result}</td>
|
|
<td style={styles.tdDate}>{formatDate(record.created_at)}</td>
|
|
</tr>
|
|
))}
|
|
{(!sumData || sumData.records.length === 0) && (
|
|
<tr>
|
|
<td colSpan={5} style={styles.emptyRow}>No records yet</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{sumData && sumData.total_pages > 1 && (
|
|
<div style={styles.pagination}>
|
|
<button
|
|
onClick={() => setSumPage((p) => Math.max(1, p - 1))}
|
|
disabled={sumPage === 1}
|
|
style={styles.pageBtn}
|
|
>
|
|
←
|
|
</button>
|
|
<span style={styles.pageInfo}>
|
|
{sumPage} / {sumData.total_pages}
|
|
</span>
|
|
<button
|
|
onClick={() => setSumPage((p) => Math.min(sumData.total_pages, p + 1))}
|
|
disabled={sumPage === sumData.total_pages}
|
|
style={styles.pageBtn}
|
|
>
|
|
→
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const styles: Record<string, React.CSSProperties> = {
|
|
main: {
|
|
minHeight: "100vh",
|
|
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
},
|
|
loader: {
|
|
flex: 1,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
fontSize: "1.125rem",
|
|
},
|
|
header: {
|
|
padding: "1.5rem 2rem",
|
|
borderBottom: "1px solid rgba(255, 255, 255, 0.06)",
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
},
|
|
nav: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.75rem",
|
|
},
|
|
navLink: {
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
fontSize: "0.875rem",
|
|
textDecoration: "none",
|
|
transition: "color 0.2s",
|
|
},
|
|
navDivider: {
|
|
color: "rgba(255, 255, 255, 0.2)",
|
|
fontSize: "0.75rem",
|
|
},
|
|
navCurrent: {
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
color: "#a78bfa",
|
|
fontSize: "0.875rem",
|
|
fontWeight: 600,
|
|
},
|
|
userInfo: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "1rem",
|
|
},
|
|
userEmail: {
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
color: "rgba(255, 255, 255, 0.6)",
|
|
fontSize: "0.875rem",
|
|
},
|
|
logoutBtn: {
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
padding: "0.5rem 1rem",
|
|
fontSize: "0.875rem",
|
|
fontWeight: 500,
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
borderRadius: "8px",
|
|
cursor: "pointer",
|
|
transition: "all 0.2s",
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
padding: "2rem",
|
|
overflowY: "auto",
|
|
},
|
|
tablesContainer: {
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "2rem",
|
|
maxWidth: "1200px",
|
|
margin: "0 auto",
|
|
},
|
|
tableCard: {
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
backdropFilter: "blur(10px)",
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
borderRadius: "20px",
|
|
padding: "1.5rem",
|
|
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
},
|
|
tableHeader: {
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: "1rem",
|
|
},
|
|
tableTitle: {
|
|
fontFamily: "'Instrument Serif', Georgia, serif",
|
|
fontSize: "1.5rem",
|
|
fontWeight: 400,
|
|
color: "#fff",
|
|
margin: 0,
|
|
},
|
|
totalCount: {
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
fontSize: "0.875rem",
|
|
color: "rgba(255, 255, 255, 0.4)",
|
|
},
|
|
tableWrapper: {
|
|
overflowX: "auto",
|
|
},
|
|
table: {
|
|
width: "100%",
|
|
borderCollapse: "collapse",
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
},
|
|
th: {
|
|
textAlign: "left",
|
|
padding: "0.75rem 1rem",
|
|
fontSize: "0.75rem",
|
|
fontWeight: 600,
|
|
color: "rgba(255, 255, 255, 0.4)",
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.05em",
|
|
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
|
|
},
|
|
tr: {
|
|
borderBottom: "1px solid rgba(255, 255, 255, 0.04)",
|
|
},
|
|
td: {
|
|
padding: "0.875rem 1rem",
|
|
fontSize: "0.875rem",
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
},
|
|
tdNum: {
|
|
padding: "0.875rem 1rem",
|
|
fontSize: "0.875rem",
|
|
color: "rgba(255, 255, 255, 0.9)",
|
|
fontFamily: "'DM Sans', monospace",
|
|
},
|
|
tdResult: {
|
|
padding: "0.875rem 1rem",
|
|
fontSize: "0.875rem",
|
|
color: "#a78bfa",
|
|
fontWeight: 600,
|
|
fontFamily: "'DM Sans', monospace",
|
|
},
|
|
tdDate: {
|
|
padding: "0.875rem 1rem",
|
|
fontSize: "0.75rem",
|
|
color: "rgba(255, 255, 255, 0.4)",
|
|
},
|
|
emptyRow: {
|
|
padding: "2rem 1rem",
|
|
textAlign: "center",
|
|
color: "rgba(255, 255, 255, 0.3)",
|
|
fontSize: "0.875rem",
|
|
},
|
|
pagination: {
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
gap: "1rem",
|
|
marginTop: "1rem",
|
|
paddingTop: "1rem",
|
|
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
|
|
},
|
|
pageBtn: {
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
padding: "0.5rem 1rem",
|
|
fontSize: "1rem",
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
color: "rgba(255, 255, 255, 0.7)",
|
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
borderRadius: "8px",
|
|
cursor: "pointer",
|
|
transition: "all 0.2s",
|
|
},
|
|
pageInfo: {
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
fontSize: "0.875rem",
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
},
|
|
};
|
|
|