2import { useState, useEffect } from "react";
3import { apiClient } from "@/lib/client/apiClient";
4import { Icon } from '@/contexts/IconContext';
6interface LoginLogEntry {
7 f110: number; // Staff ID
8 f210: string; // Staff Name
9 f211: string; // Type (login/logout/in/out)
10 f112: string; // DateTime
14interface LoginLogDetail {
15 f110: number; // Staff ID
16 f210: string; // Staff Name
17 f111s: string; // Type
18 f112: string; // DateTime
22interface TransactionEntry {
29 status: "success" | "error" | "warning";
32export default function AuditPage() {
33 const [activeTab, setActiveTab] = useState<"staff" | "transactions">("staff");
35 // Staff Login Log State
36 const [loginLogs, setLoginLogs] = useState<LoginLogEntry[]>([]);
37 const [filteredLogs, setFilteredLogs] = useState<LoginLogEntry[]>([]);
38 const [loadingStaff, setLoadingStaff] = useState(true);
39 const [staffSearchTerm, setStaffSearchTerm] = useState("");
40 const [fromDate, setFromDate] = useState("");
41 const [toDate, setToDate] = useState("");
42 const [filterType, setFilterType] = useState<string>("all");
43 const [showDetailModal, setShowDetailModal] = useState(false);
44 const [selectedLogEntry, setSelectedLogEntry] = useState<LoginLogEntry | null>(null);
45 const [loginDetails, setLoginDetails] = useState<LoginLogDetail[]>([]);
47 // Transaction Log State
48 const [transactionLog, setTransactionLog] = useState<TransactionEntry[]>([]);
49 const [filteredTransactions, setFilteredTransactions] = useState<TransactionEntry[]>([]);
50 const [loadingTransactions, setLoadingTransactions] = useState(true);
51 const [transactionFilter, setTransactionFilter] = useState<string>("all");
52 const [transactionSearchTerm, setTransactionSearchTerm] = useState("");
55 // Set default from date to beginning of month
56 const today = new Date();
57 const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
58 setFromDate(firstDay.toISOString().split('T')[0]);
59 setToDate(today.toISOString().split('T')[0]);
67 }, [fromDate, toDate]);
71 }, [loginLogs, staffSearchTerm, filterType]);
74 applyTransactionFilters();
75 }, [transactionLog, transactionSearchTerm, transactionFilter]);
77 const loadLoginLogs = async () => {
79 setLoadingStaff(true);
80 const params: any = {};
81 if (fromDate) params.fromDate = fromDate;
82 if (toDate) params.toDate = toDate;
84 const result = await apiClient.getStaffUsage(params);
86 if (result.success && result.data?.DATS) {
87 setLoginLogs(result.data.DATS);
90 console.error("Error loading login logs:", error);
92 setLoadingStaff(false);
96 const loadTransactionLog = async () => {
98 setLoadingTransactions(true);
99 // Simulate loading transaction log with mock data
100 // In production, this would fetch from an API
101 const mockData: TransactionEntry[] = [
104 timestamp: new Date("2024-12-24T10:30:00"),
105 user: "admin@everydaypos.com",
107 resource: "Authentication",
108 details: "User logged in successfully",
113 timestamp: new Date("2024-12-24T10:35:00"),
114 user: "admin@everydaypos.com",
115 action: "STOCK_ADJUSTMENT",
116 resource: "Products",
117 details: "Adjusted stock for Product ID 12345 by +10",
122 timestamp: new Date("2024-12-24T10:40:00"),
123 user: "admin@everydaypos.com",
124 action: "SALE_CREATED",
126 details: "Created sale SID 98765 for $125.50",
131 timestamp: new Date("2024-12-24T10:45:00"),
132 user: "admin@everydaypos.com",
134 resource: "Fieldpine API",
135 details: "Failed to connect to Fieldpine server - timeout",
140 timestamp: new Date("2024-12-24T10:50:00"),
141 user: "admin@everydaypos.com",
142 action: "PRODUCT_UPDATE",
143 resource: "Products",
144 details: "Updated price for Product ID 67890",
150 setTransactionLog(mockData);
151 setLoadingTransactions(false);
154 console.error("Error loading transaction log:", error);
155 setLoadingTransactions(false);
159 const applyFilters = () => {
160 let filtered = loginLogs;
163 if (filterType !== "all") {
164 filtered = filtered.filter((log) => {
165 const type = log.f211?.toLowerCase() || "";
166 if (filterType === "login") {
167 return type.includes("login") || type.includes("in");
168 } else if (filterType === "logout") {
169 return type.includes("logout") || type.includes("out");
175 // Apply search filter
176 if (staffSearchTerm) {
177 const search = staffSearchTerm.toLowerCase();
178 filtered = filtered.filter((log) => {
180 log.f110?.toString().includes(search) ||
181 log.f210?.toLowerCase().includes(search) ||
182 log.f211?.toLowerCase().includes(search) ||
183 log.f113?.toString().includes(search)
188 setFilteredLogs(filtered);
191 const applyTransactionFilters = () => {
192 let filtered = transactionLog;
194 // Apply status filter
195 if (transactionFilter !== "all") {
196 filtered = filtered.filter(entry => entry.status === transactionFilter);
199 // Apply search filter
200 if (transactionSearchTerm) {
201 const search = transactionSearchTerm.toLowerCase();
202 filtered = filtered.filter(entry =>
203 entry.action.toLowerCase().includes(search) ||
204 entry.resource.toLowerCase().includes(search) ||
205 entry.details.toLowerCase().includes(search) ||
206 entry.user.toLowerCase().includes(search)
210 setFilteredTransactions(filtered);
213 const getTransactionStatusColor = (status: string) => {
216 return "bg-success/20 text-success border-success/30";
218 return "bg-danger/20 text-danger border-danger/30";
220 return "bg-warn/20 text-warn border-warn/30";
222 return "bg-surface-2 text-muted border-border";
226 const getTransactionStatusIcon = (status: string) => {
229 return <Icon name="check_circle" size={16} />;
231 return <Icon name="cancel" size={16} />;
233 return <Icon name="warning" size={16} />;
235 return <Icon name="circle" size={16} />;
239 const openLogDetail = async (entry: LoginLogEntry) => {
240 setSelectedLogEntry(entry);
241 setShowDetailModal(true);
245 staffId: entry.f110.toString(),
250 const result = await apiClient.getStaffUsageDetail(params);
252 if (result.success && result.data?.DATS) {
253 setLoginDetails(result.data.DATS);
256 console.error("Error loading login details:", error);
260 const formatDateTime = (dateStr: string) => {
261 if (!dateStr) return "—";
262 const date = new Date(dateStr);
263 return date.toLocaleString("en-GB", {
272 const getActivityType = (type: string): "login" | "logout" | "other" => {
273 const t = type?.toLowerCase() || "";
274 if (t.includes("login") || t === "in") return "login";
275 if (t.includes("logout") || t === "out") return "logout";
279 const loginCount = loginLogs.filter(log => getActivityType(log.f211) === "login").length;
280 const logoutCount = loginLogs.filter(log => getActivityType(log.f211) === "logout").length;
282 const loading = activeTab === "staff" ? loadingStaff : loadingTransactions;
285 <div className="min-h-screen bg-bg">
286 <div className="max-w-7xl mx-auto p-6">
288 <div className="mb-8">
289 <h1 className="text-3xl font-bold text-text mb-2">System Audit Logs</h1>
290 <p className="text-muted">Monitor staff activity and system transactions</p>
294 <div className="mb-6 border-b border-border">
295 <nav className="-mb-px flex space-x-8">
297 onClick={() => setActiveTab("staff")}
298 className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
299 activeTab === "staff"
300 ? "border-brand text-brand"
301 : "border-transparent text-muted hover:text-text hover:border-border"
307 onClick={() => setActiveTab("transactions")}
308 className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors ${
309 activeTab === "transactions"
310 ? "border-brand text-brand"
311 : "border-transparent text-muted hover:text-text hover:border-border"
319 {/* Staff Login Filters */}
320 {activeTab === "staff" && (
321 <div className="bg-surface rounded-lg shadow-sm border border-border p-4 mb-6">
322 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
324 <label className="block text-sm font-medium text-text mb-2">
330 onChange={(e) => setFromDate(e.target.value)}
331 className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
335 <label className="block text-sm font-medium text-text mb-2">
341 onChange={(e) => setToDate(e.target.value)}
342 className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
346 <label className="block text-sm font-medium text-text mb-2">
351 onChange={(e) => setFilterType(e.target.value)}
352 className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
354 <option value="all">All Activity</option>
355 <option value="login">Login Only</option>
356 <option value="logout">Logout Only</option>
360 <label className="block text-sm font-medium text-text mb-2">
365 value={staffSearchTerm}
366 onChange={(e) => setStaffSearchTerm(e.target.value)}
367 placeholder="Search staff, lane..."
368 className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
375 {/* Transaction Filters */}
376 {activeTab === "transactions" && (
377 <div className="bg-surface rounded-lg shadow-sm border border-border p-4 mb-6">
378 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
380 <label className="block text-sm font-medium text-text mb-2">
385 value={transactionSearchTerm}
386 onChange={(e) => setTransactionSearchTerm(e.target.value)}
387 placeholder="Search actions, resources, or details..."
388 className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
392 <label className="block text-sm font-medium text-text mb-2">
396 value={transactionFilter}
397 onChange={(e) => setTransactionFilter(e.target.value)}
398 className="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-brand bg-surface text-text"
400 <option value="all">All Status</option>
401 <option value="success">Success</option>
402 <option value="error">Error</option>
403 <option value="warning">Warning</option>
410 {/* Stats - Staff Login */}
411 {activeTab === "staff" && (
412 <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
413 <div className="bg-surface rounded-lg shadow-sm border border-border p-4">
414 <div className="text-sm text-muted mb-1">Total Events</div>
415 <div className="text-2xl font-bold text-text">{loginLogs.length}</div>
417 <div className="bg-success/10 rounded-lg shadow-sm border border-success/30 p-4">
418 <div className="text-sm text-success mb-1">Logins</div>
419 <div className="text-2xl font-bold text-success">{loginCount}</div>
421 <div className="bg-danger/10 rounded-lg shadow-sm border border-danger/30 p-4">
422 <div className="text-sm text-danger mb-1">Logouts</div>
423 <div className="text-2xl font-bold text-danger">{logoutCount}</div>
425 <div className="bg-info/10 rounded-lg shadow-sm border border-info/30 p-4">
426 <div className="text-sm text-info mb-1">Filtered Results</div>
427 <div className="text-2xl font-bold text-info">{filteredLogs.length}</div>
432 {/* Stats - Transactions */}
433 {activeTab === "transactions" && (
434 <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
435 <div className="bg-surface rounded-lg shadow-sm border border-border p-4">
436 <div className="text-sm text-muted mb-1">Total Events</div>
437 <div className="text-2xl font-bold text-text">{transactionLog.length}</div>
439 <div className="bg-success/10 rounded-lg shadow-sm border border-success/30 p-4">
440 <div className="text-sm text-success mb-1">Success</div>
441 <div className="text-2xl font-bold text-success">
442 {transactionLog.filter(e => e.status === "success").length}
445 <div className="bg-danger/10 rounded-lg shadow-sm border border-danger/30 p-4">
446 <div className="text-sm text-danger mb-1">Errors</div>
447 <div className="text-2xl font-bold text-danger">
448 {transactionLog.filter(e => e.status === "error").length}
451 <div className="bg-warn/10 rounded-lg shadow-sm border border-warn/30 p-4">
452 <div className="text-sm text-warn mb-1">Warnings</div>
453 <div className="text-2xl font-bold text-warn">
454 {transactionLog.filter(e => e.status === "warning").length}
460 {/* Staff Login Activity Table */}
461 {activeTab === "staff" && (
462 <div className="bg-surface rounded-lg shadow-sm border border-border overflow-hidden">
464 <div className="p-12 text-center text-muted">
465 Loading staff activity log...
467 ) : filteredLogs.length === 0 ? (
468 <div className="p-12 text-center text-muted">
469 {staffSearchTerm ? `No events match "${staffSearchTerm}"` : "No activity found in the selected date range"}
472 <div className="overflow-x-auto">
473 <table className="min-w-full divide-y divide-gray-200">
474 <thead className="bg-surface-2">
476 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
479 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
482 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
485 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
488 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
493 <tbody className="bg-surface divide-y divide-gray-200">
494 {filteredLogs.map((log, idx) => {
495 const activityType = getActivityType(log.f211);
497 <tr key={idx} className="hover:bg-surface-2">
498 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
499 {formatDateTime(log.f112)}
501 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
502 <div className="font-medium text-text">{log.f210 || "—"}</div>
503 <div className="text-xs text-muted">ID: {log.f110}</div>
505 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
508 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
511 <td className="px-6 py-4 whitespace-nowrap">
513 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${
514 activityType === "login"
515 ? "bg-success/20 text-success border-success/30"
516 : activityType === "logout"
517 ? "bg-danger/20 text-danger border-danger/30"
518 : "bg-surface-2 text-muted border-border"
521 <span className="mr-1">
522 {activityType === "login" ? <Icon name="check" size={12} /> : activityType === "logout" ? <Icon name="close" size={12} /> : <Icon name="circle" size={12} />}
525 onClick={() => openLogDetail(log)}
526 className="hover:underline"
542 {/* System Transactions Table */}
543 {activeTab === "transactions" && (
544 <div className="bg-surface rounded-lg shadow-sm border border-border overflow-hidden">
545 {loadingTransactions ? (
546 <div className="p-12 text-center text-muted">
547 Loading transaction log...
549 ) : filteredTransactions.length === 0 ? (
550 <div className="p-12 text-center text-muted">
551 {transactionSearchTerm ? `No events match "${transactionSearchTerm}"` : "No events found"}
554 <div className="overflow-x-auto">
555 <table className="min-w-full divide-y divide-gray-200">
556 <thead className="bg-surface-2">
558 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
561 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
564 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
567 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
570 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
573 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
578 <tbody className="bg-surface divide-y divide-gray-200">
579 {filteredTransactions.map((entry) => (
580 <tr key={entry.id} className="hover:bg-surface-2">
581 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
582 {entry.timestamp.toLocaleString()}
584 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
587 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
590 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
593 <td className="px-6 py-4 text-sm text-text">
596 <td className="px-6 py-4 whitespace-nowrap">
598 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getTransactionStatusColor(
602 <span className="mr-1">{getTransactionStatusIcon(entry.status)}</span>
603 {entry.status.toUpperCase()}
616 <div className="mt-6 bg-info/10 border border-info/30 rounded-lg p-4">
617 {activeTab === "staff" ? (
619 <h3 className="font-semibold text-info mb-2">About Staff Activity Log</h3>
620 <p className="text-info text-sm mb-2">
621 The staff activity log tracks all staff login and logout events across POS terminals. This helps with:
623 <ul className="list-disc list-inside text-info text-sm space-y-1 ml-2">
624 <li>Security monitoring and compliance</li>
625 <li>Staff attendance tracking</li>
626 <li>Terminal usage analysis</li>
627 <li>Shift management and scheduling</li>
629 <p className="text-info text-xs mt-3">
630 <strong>Note:</strong> Data is fetched from the Fieldpine BUCK API (getStaffUsage). Click "View Details" to see complete activity history for a staff member.
635 <h3 className="font-semibold text-info mb-2">About System Transactions</h3>
636 <p className="text-info text-sm mb-2">
637 The transaction log tracks all system activities including user actions, API calls, and system events. This helps with:
639 <ul className="list-disc list-inside text-info text-sm space-y-1 ml-2">
640 <li>Security monitoring and compliance</li>
641 <li>Debugging and troubleshooting</li>
642 <li>Performance analysis</li>
643 <li>User activity tracking</li>
645 <p className="text-info text-xs mt-3">
646 <strong>Note:</strong> Currently showing mock data. Production will log real transactions.
654 {showDetailModal && selectedLogEntry && (
655 <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
656 <div className="bg-surface rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-hidden">
657 <div className="bg-gradient-to-r from-brand to-brand-2 text-surface px-6 py-4 flex items-center justify-between">
658 <h2 className="text-xl font-bold">
659 Activity Details - {selectedLogEntry.f210} (ID: {selectedLogEntry.f110})
662 onClick={() => setShowDetailModal(false)}
663 className="text-surface hover:opacity-80 transition-opacity"
665 <Icon name="close" size={24} />
669 <div className="p-6 overflow-y-auto max-h-[calc(80vh-80px)]">
670 {loginDetails.length === 0 ? (
671 <div className="text-center py-8">
672 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
673 <p className="text-muted">Loading activity details...</p>
677 <div className="mb-4 bg-surface-2 rounded-lg p-4">
678 <div className="grid grid-cols-2 gap-4 text-sm">
680 <span className="text-muted">Staff Name:</span>
681 <span className="ml-2 font-semibold text-text">{selectedLogEntry.f210}</span>
684 <span className="text-muted">Staff ID:</span>
685 <span className="ml-2 font-semibold text-text">{selectedLogEntry.f110}</span>
688 <span className="text-muted">Date Range:</span>
689 <span className="ml-2 font-semibold text-text">{fromDate} to {toDate}</span>
692 <span className="text-muted">Total Events:</span>
693 <span className="ml-2 font-semibold text-text">{loginDetails.length}</span>
697 <table className="w-full border-collapse">
698 <thead className="bg-surface-2">
700 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider border-b">Action</th>
701 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider border-b">Date & Time</th>
702 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider border-b">Lane</th>
705 <tbody className="divide-y divide-gray-200">
706 {loginDetails.map((detail, idx) => {
707 const activityType = getActivityType(detail.f111s);
709 <tr key={idx} className="hover:bg-surface-2">
710 <td className="px-4 py-3 text-sm">
712 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${
713 activityType === "login"
714 ? "bg-success/20 text-success border-success/30"
715 : activityType === "logout"
716 ? "bg-danger/20 text-danger border-danger/30"
717 : "bg-surface-2 text-muted border-border"
720 <span className="mr-1">
721 {activityType === "login" ? <Icon name="check" size={12} /> : activityType === "logout" ? <Icon name="close" size={12} /> : <Icon name="circle" size={12} />}
723 {detail.f111s || "—"}
726 <td className="px-4 py-3 text-sm text-muted">
727 {formatDateTime(detail.f112)}
729 <td className="px-4 py-3 text-sm text-muted">