1import { a as __toESM } from "./rolldown-runtime-_TIqcEvS.js"; 2import { C as require_jsx_runtime, u as RefreshCw, w as require_react } from "./admin-attendance-Devs9Lzj.js"; 3import { C as ChevronDown, S as ChevronUp } from "./admin-audit-CGi9m8Nk.js"; 4import { Et as MessageSquare } from "./admin-components-CkAH8kJs.js"; 5import { f as DollarSign, g as Award } from "./admin-budget-1k-XVOid.js"; 6import { r as UserPlus } from "./admin-broadcast-Dm1KhKbJ.js"; 7import { i as CalendarCheck } from "./enroll-page-CX5lPbth.js"; 8//#region src/components/StudentTimeline.tsx 9var import_react = /* @__PURE__ */ __toESM(require_react(), 1); 10var import_jsx_runtime = require_jsx_runtime(); 11var TYPE_CONFIG = { 12 enrollment: { 13 icon: UserPlus, 14 color: "text-primary", 15 dot: "bg-primary" 16 }, 17 note: { 18 icon: MessageSquare, 19 color: "text-blue-600", 20 dot: "bg-blue-500" 21 }, 22 payment: { 23 icon: DollarSign, 24 color: "text-green-600", 25 dot: "bg-green-500" 26 }, 27 attendance: { 28 icon: CalendarCheck, 29 color: "text-purple-600", 30 dot: "bg-purple-500" 31 }, 32 status: { 33 icon: Award, 34 color: "text-orange-600", 35 dot: "bg-orange-500" 36 } 37}; 38var STATUS_COLORS = { 39 present: "bg-green-100 text-green-700", 40 late: "bg-yellow-100 text-yellow-700", 41 excused: "bg-blue-100 text-blue-700", 42 absent: "bg-red-100 text-red-700", 43 completed: "bg-green-100 text-green-700", 44 pending: "bg-yellow-100 text-yellow-700", 45 failed: "bg-red-100 text-red-700" 46}; 47function StudentTimeline({ studentId }) { 48 const [events, setEvents] = (0, import_react.useState)([]); 49 const [loading, setLoading] = (0, import_react.useState)(true); 50 const [expanded, setExpanded] = (0, import_react.useState)(false); 51 const PREVIEW_COUNT = 8; 52 const load = async () => { 53 setLoading(true); 54 try { 55 const res = await fetch(`/api/students/${studentId}/timeline`); 56 if (res.ok) setEvents((await res.json()).events ?? []); 57 } finally { 58 setLoading(false); 59 } 60 }; 61 (0, import_react.useEffect)(() => { 62 load(); 63 }, [studentId]); 64 const visible = expanded ? events : events.slice(0, PREVIEW_COUNT); 65 if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 66 className: "text-xs text-gray-400 py-3 animate-pulse", 67 children: "Loading timelineâ¦" 68 }); 69 if (events.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 70 className: "text-xs text-gray-400 py-3", 71 children: "No activity recorded yet." 72 }); 73 return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [ 74 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 75 className: "flex items-center justify-between mb-3", 76 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 77 className: "text-xs font-semibold text-gray-500 uppercase tracking-wide", 78 children: "Activity Timeline" 79 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { 80 onClick: () => void load(), 81 className: "text-gray-400 hover:text-primary", 82 children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { size: 12 }) 83 })] 84 }), 85 /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 86 className: "relative", 87 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute left-[7px] top-2 bottom-2 w-px bg-gray-200" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { 88 className: "space-y-3", 89 children: visible.map((event) => { 90 const cfg = TYPE_CONFIG[event.type] ?? TYPE_CONFIG.note; 91 const Icon = cfg.icon; 92 return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 93 className: "flex gap-3 relative", 94 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `w-3.5 h-3.5 rounded-full shrink-0 mt-0.5 z-10 ring-2 ring-white ${cfg.dot}` }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 95 className: "flex-1 min-w-0 pb-1", 96 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 97 className: "flex items-start justify-between gap-2", 98 children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { 99 className: "flex items-center gap-1.5 min-w-0", 100 children: [ 101 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { 102 size: 11, 103 className: `shrink-0 ${cfg.color}` 104 }),
105 /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 106 className: "text-xs font-semibold text-gray-800 truncate", 107 children: event.title 108 }), 109 event.meta && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 110 className: `text-xs px-1.5 py-0.5 rounded font-medium ${STATUS_COLORS[event.meta.toLowerCase()] ?? "bg-gray-100 text-gray-500"}`, 111 children: event.meta 112 }) 113 ] 114 }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { 115 className: "text-xs text-gray-400 whitespace-nowrap shrink-0", 116 children: formatRelative(event.timestamp) 117 })] 118 }), event.detail && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { 119 className: "text-xs text-gray-500 mt-0.5 leading-relaxed line-clamp-2", 120 children: event.detail 121 })] 122 })] 123 }, event.id); 124 }) 125 })] 126 }), 127 events.length > PREVIEW_COUNT && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { 128 onClick: () => setExpanded(!expanded), 129 className: "mt-3 flex items-center gap-1 text-xs text-primary hover:underline", 130 children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronUp, { size: 12 }), " Show less"] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
131 /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, { size: 12 }), 132 " Show ", 133 events.length - PREVIEW_COUNT, 134 " more events" 135 ] }) 136 }) 137 ] }); 138} 139function formatRelative(timestamp) { 140 const date = new Date(timestamp); 141 const diff = Date.now() - date.getTime(); 142 const mins = Math.floor(diff / 6e4); 143 const hours = Math.floor(diff / 36e5); 144 const days = Math.floor(diff / 864e5); 145 if (mins < 1) return "just now"; 146 if (mins < 60) return `${mins}m ago`; 147 if (hours < 24) return `${hours}h ago`; 148 if (days < 7) return `${days}d ago`; 149 return date.toLocaleDateString("en-US", { 150 month: "short", 151 day: "numeric", 152 year: days > 365 ? "numeric" : void 0 153 }); 154} 155//#endregion 156export { StudentTimeline as default };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.