EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2import { useState, useEffect } from "react";
3import { apiClient } from "@/lib/client/apiClient";
4import { Icon } from '@/contexts/IconContext';
5
6interface LoginLogEntry {
7 f110: number; // Staff ID
8 f210: string; // Staff Name
9 f211: string; // Type (login/logout/in/out)
10 f112: string; // DateTime
11 f113: number; // Lane
12}
13
14interface LoginLogDetail {
15 f110: number; // Staff ID
16 f210: string; // Staff Name
17 f111s: string; // Type
18 f112: string; // DateTime
19 f113: number; // Lane
20}
21
22interface TransactionEntry {
23 id: string;
24 timestamp: Date;
25 user: string;
26 action: string;
27 resource: string;
28 details: string;
29 status: "success" | "error" | "warning";
30}
31
32export default function AuditPage() {
33 const [activeTab, setActiveTab] = useState<"staff" | "transactions">("staff");
34
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[]>([]);
46
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("");
53
54 useEffect(() => {
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]);
60
61 loadLoginLogs();
62 loadTransactionLog();
63 }, []);
64
65 useEffect(() => {
66 loadLoginLogs();
67 }, [fromDate, toDate]);
68
69 useEffect(() => {
70 applyFilters();
71 }, [loginLogs, staffSearchTerm, filterType]);
72
73 useEffect(() => {
74 applyTransactionFilters();
75 }, [transactionLog, transactionSearchTerm, transactionFilter]);
76
77 const loadLoginLogs = async () => {
78 try {
79 setLoadingStaff(true);
80 const params: any = {};
81 if (fromDate) params.fromDate = fromDate;
82 if (toDate) params.toDate = toDate;
83
84 const result = await apiClient.getStaffUsage(params);
85
86 if (result.success && result.data?.DATS) {
87 setLoginLogs(result.data.DATS);
88 }
89 } catch (error) {
90 console.error("Error loading login logs:", error);
91 } finally {
92 setLoadingStaff(false);
93 }
94 };
95
96 const loadTransactionLog = async () => {
97 try {
98 setLoadingTransactions(true);
99 // Simulate loading transaction log with mock data
100 // In production, this would fetch from an API
101 const mockData: TransactionEntry[] = [
102 {
103 id: "1",
104 timestamp: new Date("2024-12-24T10:30:00"),
105 user: "admin@everydaypos.com",
106 action: "LOGIN",
107 resource: "Authentication",
108 details: "User logged in successfully",
109 status: "success"
110 },
111 {
112 id: "2",
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",
118 status: "success"
119 },
120 {
121 id: "3",
122 timestamp: new Date("2024-12-24T10:40:00"),
123 user: "admin@everydaypos.com",
124 action: "SALE_CREATED",
125 resource: "Sales",
126 details: "Created sale SID 98765 for $125.50",
127 status: "success"
128 },
129 {
130 id: "4",
131 timestamp: new Date("2024-12-24T10:45:00"),
132 user: "admin@everydaypos.com",
133 action: "API_ERROR",
134 resource: "Fieldpine API",
135 details: "Failed to connect to Fieldpine server - timeout",
136 status: "error"
137 },
138 {
139 id: "5",
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",
145 status: "success"
146 }
147 ];
148
149 setTimeout(() => {
150 setTransactionLog(mockData);
151 setLoadingTransactions(false);
152 }, 500);
153 } catch (error) {
154 console.error("Error loading transaction log:", error);
155 setLoadingTransactions(false);
156 }
157 };
158
159 const applyFilters = () => {
160 let filtered = loginLogs;
161
162 // Apply type filter
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");
170 }
171 return true;
172 });
173 }
174
175 // Apply search filter
176 if (staffSearchTerm) {
177 const search = staffSearchTerm.toLowerCase();
178 filtered = filtered.filter((log) => {
179 return (
180 log.f110?.toString().includes(search) ||
181 log.f210?.toLowerCase().includes(search) ||
182 log.f211?.toLowerCase().includes(search) ||
183 log.f113?.toString().includes(search)
184 );
185 });
186 }
187
188 setFilteredLogs(filtered);
189 };
190
191 const applyTransactionFilters = () => {
192 let filtered = transactionLog;
193
194 // Apply status filter
195 if (transactionFilter !== "all") {
196 filtered = filtered.filter(entry => entry.status === transactionFilter);
197 }
198
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)
207 );
208 }
209
210 setFilteredTransactions(filtered);
211 };
212
213 const getTransactionStatusColor = (status: string) => {
214 switch (status) {
215 case "success":
216 return "bg-success/20 text-success border-success/30";
217 case "error":
218 return "bg-danger/20 text-danger border-danger/30";
219 case "warning":
220 return "bg-warn/20 text-warn border-warn/30";
221 default:
222 return "bg-surface-2 text-muted border-border";
223 }
224 };
225
226 const getTransactionStatusIcon = (status: string) => {
227 switch (status) {
228 case "success":
229 return <Icon name="check_circle" size={16} />;
230 case "error":
231 return <Icon name="cancel" size={16} />;
232 case "warning":
233 return <Icon name="warning" size={16} />;
234 default:
235 return <Icon name="circle" size={16} />;
236 }
237 };
238
239 const openLogDetail = async (entry: LoginLogEntry) => {
240 setSelectedLogEntry(entry);
241 setShowDetailModal(true);
242
243 try {
244 const params = {
245 staffId: entry.f110.toString(),
246 fromDate,
247 toDate,
248 };
249
250 const result = await apiClient.getStaffUsageDetail(params);
251
252 if (result.success && result.data?.DATS) {
253 setLoginDetails(result.data.DATS);
254 }
255 } catch (error) {
256 console.error("Error loading login details:", error);
257 }
258 };
259
260 const formatDateTime = (dateStr: string) => {
261 if (!dateStr) return "—";
262 const date = new Date(dateStr);
263 return date.toLocaleString("en-GB", {
264 day: "2-digit",
265 month: "2-digit",
266 year: "numeric",
267 hour: "2-digit",
268 minute: "2-digit",
269 });
270 };
271
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";
276 return "other";
277 };
278
279 const loginCount = loginLogs.filter(log => getActivityType(log.f211) === "login").length;
280 const logoutCount = loginLogs.filter(log => getActivityType(log.f211) === "logout").length;
281
282 const loading = activeTab === "staff" ? loadingStaff : loadingTransactions;
283
284 return (
285 <div className="min-h-screen bg-bg">
286 <div className="max-w-7xl mx-auto p-6">
287 {/* Header */}
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>
291 </div>
292
293 {/* Tabs */}
294 <div className="mb-6 border-b border-border">
295 <nav className="-mb-px flex space-x-8">
296 <button
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"
302 }`}
303 >
304 Staff Login Activity
305 </button>
306 <button
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"
312 }`}
313 >
314 System Transactions
315 </button>
316 </nav>
317 </div>
318
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">
323 <div>
324 <label className="block text-sm font-medium text-text mb-2">
325 From Date
326 </label>
327 <input
328 type="date"
329 value={fromDate}
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"
332 />
333 </div>
334 <div>
335 <label className="block text-sm font-medium text-text mb-2">
336 To Date
337 </label>
338 <input
339 type="date"
340 value={toDate}
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"
343 />
344 </div>
345 <div>
346 <label className="block text-sm font-medium text-text mb-2">
347 Filter by Type
348 </label>
349 <select
350 value={filterType}
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"
353 >
354 <option value="all">All Activity</option>
355 <option value="login">Login Only</option>
356 <option value="logout">Logout Only</option>
357 </select>
358 </div>
359 <div>
360 <label className="block text-sm font-medium text-text mb-2">
361 Search
362 </label>
363 <input
364 type="text"
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"
369 />
370 </div>
371 </div>
372 </div>
373 )}
374
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">
379 <div>
380 <label className="block text-sm font-medium text-text mb-2">
381 Search
382 </label>
383 <input
384 type="text"
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"
389 />
390 </div>
391 <div>
392 <label className="block text-sm font-medium text-text mb-2">
393 Filter by Status
394 </label>
395 <select
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"
399 >
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>
404 </select>
405 </div>
406 </div>
407 </div>
408 )}
409
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>
416 </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>
420 </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>
424 </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>
428 </div>
429 </div>
430 )}
431
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>
438 </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}
443 </div>
444 </div>
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}
449 </div>
450 </div>
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}
455 </div>
456 </div>
457 </div>
458 )}
459
460 {/* Staff Login Activity Table */}
461 {activeTab === "staff" && (
462 <div className="bg-surface rounded-lg shadow-sm border border-border overflow-hidden">
463 {loadingStaff ? (
464 <div className="p-12 text-center text-muted">
465 Loading staff activity log...
466 </div>
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"}
470 </div>
471 ) : (
472 <div className="overflow-x-auto">
473 <table className="min-w-full divide-y divide-gray-200">
474 <thead className="bg-surface-2">
475 <tr>
476 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
477 Timestamp
478 </th>
479 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
480 Staff
481 </th>
482 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
483 Action
484 </th>
485 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
486 Lane
487 </th>
488 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
489 Details
490 </th>
491 </tr>
492 </thead>
493 <tbody className="bg-surface divide-y divide-gray-200">
494 {filteredLogs.map((log, idx) => {
495 const activityType = getActivityType(log.f211);
496 return (
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)}
500 </td>
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>
504 </td>
505 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
506 {log.f211 || "—"}
507 </td>
508 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
509 {log.f113 || "—"}
510 </td>
511 <td className="px-6 py-4 whitespace-nowrap">
512 <span
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"
519 }`}
520 >
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} />}
523 </span>
524 <button
525 onClick={() => openLogDetail(log)}
526 className="hover:underline"
527 >
528 View Details
529 </button>
530 </span>
531 </td>
532 </tr>
533 );
534 })}
535 </tbody>
536 </table>
537 </div>
538 )}
539 </div>
540 )}
541
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...
548 </div>
549 ) : filteredTransactions.length === 0 ? (
550 <div className="p-12 text-center text-muted">
551 {transactionSearchTerm ? `No events match "${transactionSearchTerm}"` : "No events found"}
552 </div>
553 ) : (
554 <div className="overflow-x-auto">
555 <table className="min-w-full divide-y divide-gray-200">
556 <thead className="bg-surface-2">
557 <tr>
558 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
559 Timestamp
560 </th>
561 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
562 User
563 </th>
564 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
565 Action
566 </th>
567 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
568 Resource
569 </th>
570 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
571 Details
572 </th>
573 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
574 Status
575 </th>
576 </tr>
577 </thead>
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()}
583 </td>
584 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
585 {entry.user}
586 </td>
587 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-text">
588 {entry.action}
589 </td>
590 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
591 {entry.resource}
592 </td>
593 <td className="px-6 py-4 text-sm text-text">
594 {entry.details}
595 </td>
596 <td className="px-6 py-4 whitespace-nowrap">
597 <span
598 className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getTransactionStatusColor(
599 entry.status
600 )}`}
601 >
602 <span className="mr-1">{getTransactionStatusIcon(entry.status)}</span>
603 {entry.status.toUpperCase()}
604 </span>
605 </td>
606 </tr>
607 ))}
608 </tbody>
609 </table>
610 </div>
611 )}
612 </div>
613 )}
614
615 {/* Info Box */}
616 <div className="mt-6 bg-info/10 border border-info/30 rounded-lg p-4">
617 {activeTab === "staff" ? (
618 <>
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:
622 </p>
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>
628 </ul>
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.
631 </p>
632 </>
633 ) : (
634 <>
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:
638 </p>
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>
644 </ul>
645 <p className="text-info text-xs mt-3">
646 <strong>Note:</strong> Currently showing mock data. Production will log real transactions.
647 </p>
648 </>
649 )}
650 </div>
651 </div>
652
653 {/* Detail Modal */}
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})
660 </h2>
661 <button
662 onClick={() => setShowDetailModal(false)}
663 className="text-surface hover:opacity-80 transition-opacity"
664 >
665 <Icon name="close" size={24} />
666 </button>
667 </div>
668
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>
674 </div>
675 ) : (
676 <div>
677 <div className="mb-4 bg-surface-2 rounded-lg p-4">
678 <div className="grid grid-cols-2 gap-4 text-sm">
679 <div>
680 <span className="text-muted">Staff Name:</span>
681 <span className="ml-2 font-semibold text-text">{selectedLogEntry.f210}</span>
682 </div>
683 <div>
684 <span className="text-muted">Staff ID:</span>
685 <span className="ml-2 font-semibold text-text">{selectedLogEntry.f110}</span>
686 </div>
687 <div>
688 <span className="text-muted">Date Range:</span>
689 <span className="ml-2 font-semibold text-text">{fromDate} to {toDate}</span>
690 </div>
691 <div>
692 <span className="text-muted">Total Events:</span>
693 <span className="ml-2 font-semibold text-text">{loginDetails.length}</span>
694 </div>
695 </div>
696 </div>
697 <table className="w-full border-collapse">
698 <thead className="bg-surface-2">
699 <tr>
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>
703 </tr>
704 </thead>
705 <tbody className="divide-y divide-gray-200">
706 {loginDetails.map((detail, idx) => {
707 const activityType = getActivityType(detail.f111s);
708 return (
709 <tr key={idx} className="hover:bg-surface-2">
710 <td className="px-4 py-3 text-sm">
711 <span
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"
718 }`}
719 >
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} />}
722 </span>
723 {detail.f111s || "—"}
724 </span>
725 </td>
726 <td className="px-4 py-3 text-sm text-muted">
727 {formatDateTime(detail.f112)}
728 </td>
729 <td className="px-4 py-3 text-sm text-muted">
730 {detail.f113 || "—"}
731 </td>
732 </tr>
733 );
734 })}
735 </tbody>
736 </table>
737 </div>
738 )}
739 </div>
740 </div>
741 </div>
742 )}
743 </div>
744 );
745}