Spaces:
Sleeping
Sleeping
quachtiensinh27 commited on
Commit ·
477a7c3
1
Parent(s): 57f5158
cập nhật ui dashboard
Browse files- src/App.jsx +5 -0
- src/components/Sidebar.jsx +12 -1
- src/components/sidebar/SpaceIcon.jsx +2 -1
- src/pages/TADashboard.css +429 -0
- src/pages/TADashboard.jsx +400 -0
- src/store/slices/appSlice.js +5 -0
src/App.jsx
CHANGED
|
@@ -13,6 +13,7 @@ import CreateAgentTips from "./components/createspace/CreateAgentTips";
|
|
| 13 |
import ManageAgent from "./components/createspace/ManageAgent";
|
| 14 |
import ManageAgentTips from "./components/createspace/ManageAgentTips";
|
| 15 |
import LoginPage from "./pages/LoginPage";
|
|
|
|
| 16 |
import AppLoadingScreen from "./components/AppLoadingScreen";
|
| 17 |
import { initializeAuth } from "./store/slices/authSlice";
|
| 18 |
import {
|
|
@@ -346,6 +347,10 @@ function App() {
|
|
| 346 |
{renderCreateContent()}
|
| 347 |
{renderCreateTips()}
|
| 348 |
</>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
) : (
|
| 350 |
<>
|
| 351 |
<div
|
|
|
|
| 13 |
import ManageAgent from "./components/createspace/ManageAgent";
|
| 14 |
import ManageAgentTips from "./components/createspace/ManageAgentTips";
|
| 15 |
import LoginPage from "./pages/LoginPage";
|
| 16 |
+
import TADashboard from "./pages/TADashboard";
|
| 17 |
import AppLoadingScreen from "./components/AppLoadingScreen";
|
| 18 |
import { initializeAuth } from "./store/slices/authSlice";
|
| 19 |
import {
|
|
|
|
| 347 |
{renderCreateContent()}
|
| 348 |
{renderCreateTips()}
|
| 349 |
</>
|
| 350 |
+
) : currentView === "dashboard" ? (
|
| 351 |
+
<>
|
| 352 |
+
<TADashboard />
|
| 353 |
+
</>
|
| 354 |
) : (
|
| 355 |
<>
|
| 356 |
<div
|
src/components/Sidebar.jsx
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
| 8 |
openCreateSpace,
|
| 9 |
openSettings,
|
| 10 |
closeSettings,
|
|
|
|
| 11 |
} from "../store/slices/appSlice";
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
function Sidebar() {
|
|
@@ -20,7 +22,7 @@ function Sidebar() {
|
|
| 20 |
const { spaces, loading: spacesLoading, spacesFetched } = useSelector(
|
| 21 |
(state) => state.space,
|
| 22 |
);
|
| 23 |
-
const { isAuthenticated } = useSelector((state) => state.auth);
|
| 24 |
const currentView = isSettings ? "settings" : activeView;
|
| 25 |
|
| 26 |
console.log("[Sidebar] Render - spaces count:", spaces.length, "spacesLoading:", spacesLoading, "spaces:", spaces.map(s => ({ id: s.id, name: s.name })));
|
|
@@ -65,6 +67,15 @@ function Sidebar() {
|
|
| 65 |
/>
|
| 66 |
</div>
|
| 67 |
<div className="flex flex-col items-center gap-2">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
<SpaceIcon
|
| 69 |
icon="settings"
|
| 70 |
isActive={currentView === "settings"}
|
|
|
|
| 8 |
openCreateSpace,
|
| 9 |
openSettings,
|
| 10 |
closeSettings,
|
| 11 |
+
navigateToDashboard,
|
| 12 |
} from "../store/slices/appSlice";
|
| 13 |
+
import { FiBarChart2 } from "react-icons/fi";
|
| 14 |
|
| 15 |
|
| 16 |
function Sidebar() {
|
|
|
|
| 22 |
const { spaces, loading: spacesLoading, spacesFetched } = useSelector(
|
| 23 |
(state) => state.space,
|
| 24 |
);
|
| 25 |
+
const { isAuthenticated, user } = useSelector((state) => state.auth);
|
| 26 |
const currentView = isSettings ? "settings" : activeView;
|
| 27 |
|
| 28 |
console.log("[Sidebar] Render - spaces count:", spaces.length, "spacesLoading:", spacesLoading, "spaces:", spaces.map(s => ({ id: s.id, name: s.name })));
|
|
|
|
| 67 |
/>
|
| 68 |
</div>
|
| 69 |
<div className="flex flex-col items-center gap-2">
|
| 70 |
+
{user && activeSpace && spaces.find(s => s.id === activeSpace)?.owner_id === user.id && (activeView === "space" || activeView === "dashboard") && (
|
| 71 |
+
<SpaceIcon
|
| 72 |
+
icon="dashboard"
|
| 73 |
+
isActive={currentView === "dashboard"}
|
| 74 |
+
hasNotification={false}
|
| 75 |
+
onClick={() => dispatch(navigateToDashboard())}
|
| 76 |
+
title="TA Dashboard"
|
| 77 |
+
/>
|
| 78 |
+
)}
|
| 79 |
<SpaceIcon
|
| 80 |
icon="settings"
|
| 81 |
isActive={currentView === "settings"}
|
src/components/sidebar/SpaceIcon.jsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
-
import { FiPlus, FiSettings } from "react-icons/fi";
|
| 2 |
import { getSpaceIconComponent } from "../../constants/spaceIcons";
|
| 3 |
|
| 4 |
// Built-in system icons (not from space registry)
|
| 5 |
const systemIcons = {
|
| 6 |
plus: FiPlus,
|
| 7 |
settings: FiSettings,
|
|
|
|
| 8 |
};
|
| 9 |
|
| 10 |
function SpaceIcon({ icon, name, isActive, hasNotification, onClick, title }) {
|
|
|
|
| 1 |
+
import { FiPlus, FiSettings, FiBarChart2 } from "react-icons/fi";
|
| 2 |
import { getSpaceIconComponent } from "../../constants/spaceIcons";
|
| 3 |
|
| 4 |
// Built-in system icons (not from space registry)
|
| 5 |
const systemIcons = {
|
| 6 |
plus: FiPlus,
|
| 7 |
settings: FiSettings,
|
| 8 |
+
dashboard: FiBarChart2,
|
| 9 |
};
|
| 10 |
|
| 11 |
function SpaceIcon({ icon, name, isActive, hasNotification, onClick, title }) {
|
src/pages/TADashboard.css
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--ta-bg: #0f1117;
|
| 3 |
+
--ta-bg2: #1a1d27;
|
| 4 |
+
--ta-bg3: #242836;
|
| 5 |
+
--ta-bg4: #2e3347;
|
| 6 |
+
--ta-border: #2e3347;
|
| 7 |
+
--ta-border2: #3d4460;
|
| 8 |
+
--ta-text: #e8eaed;
|
| 9 |
+
--ta-text2: #9aa0b4;
|
| 10 |
+
--ta-text3: #636b83;
|
| 11 |
+
--ta-accent: #22c55e;
|
| 12 |
+
--ta-accent2: #16a34a;
|
| 13 |
+
--ta-accent-bg: rgba(34, 197, 94, 0.12);
|
| 14 |
+
--ta-red: #ef4444;
|
| 15 |
+
--ta-red-bg: rgba(239, 68, 68, 0.12);
|
| 16 |
+
--ta-amber: #f59e0b;
|
| 17 |
+
--ta-amber-bg: rgba(245, 158, 11, 0.12);
|
| 18 |
+
--ta-blue: #3b82f6;
|
| 19 |
+
--ta-blue-bg: rgba(59, 130, 246, 0.12);
|
| 20 |
+
--ta-purple: #a855f7;
|
| 21 |
+
--ta-purple-bg: rgba(168, 85, 247, 0.12);
|
| 22 |
+
--ta-radius: 8px;
|
| 23 |
+
--ta-radius-lg: 12px;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.ta-dashboard-container {
|
| 27 |
+
display: flex;
|
| 28 |
+
height: 100vh;
|
| 29 |
+
width: 100%;
|
| 30 |
+
background: var(--ta-bg);
|
| 31 |
+
color: var(--ta-text);
|
| 32 |
+
font-family: 'Inter', sans-serif;
|
| 33 |
+
overflow: hidden;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Internal Sidebar */
|
| 37 |
+
.ta-internal-sidebar {
|
| 38 |
+
width: 220px;
|
| 39 |
+
background: var(--ta-bg2);
|
| 40 |
+
border-right: 1px solid var(--ta-border);
|
| 41 |
+
display: flex;
|
| 42 |
+
flex-direction: column;
|
| 43 |
+
flex-shrink: 0;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.sb-head {
|
| 47 |
+
padding: 16px;
|
| 48 |
+
border-bottom: 1px solid var(--ta-border);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.sb-title {
|
| 52 |
+
font-size: 14px;
|
| 53 |
+
font-weight: 600;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.sb-sub {
|
| 57 |
+
font-size: 11px;
|
| 58 |
+
color: var(--ta-text3);
|
| 59 |
+
margin-top: 2px;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.sb-section {
|
| 63 |
+
font-size: 10px;
|
| 64 |
+
color: var(--ta-text3);
|
| 65 |
+
font-weight: 600;
|
| 66 |
+
padding: 12px 16px 4px;
|
| 67 |
+
letter-spacing: 0.08em;
|
| 68 |
+
text-transform: uppercase;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.sb-item {
|
| 72 |
+
padding: 10px 16px;
|
| 73 |
+
display: flex;
|
| 74 |
+
align-items: center;
|
| 75 |
+
gap: 10px;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: 0.15s;
|
| 78 |
+
font-size: 12.5px;
|
| 79 |
+
color: var(--ta-text2);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.sb-item:hover {
|
| 83 |
+
background: var(--ta-bg3);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.sb-item.active {
|
| 87 |
+
background: var(--ta-bg3);
|
| 88 |
+
border-right: 2px solid var(--ta-accent);
|
| 89 |
+
color: var(--ta-text);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.sb-icon {
|
| 93 |
+
width: 24px;
|
| 94 |
+
height: 24px;
|
| 95 |
+
border-radius: 6px;
|
| 96 |
+
display: flex;
|
| 97 |
+
align-items: center;
|
| 98 |
+
justify-content: center;
|
| 99 |
+
font-size: 12px;
|
| 100 |
+
flex-shrink: 0;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* Main Content Area */
|
| 104 |
+
.ta-main-content {
|
| 105 |
+
flex: 1;
|
| 106 |
+
display: flex;
|
| 107 |
+
flex-direction: column;
|
| 108 |
+
overflow: hidden;
|
| 109 |
+
min-width: 0;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.ta-topbar {
|
| 113 |
+
display: flex;
|
| 114 |
+
align-items: center;
|
| 115 |
+
justify-content: space-between;
|
| 116 |
+
padding: 16px 24px;
|
| 117 |
+
border-bottom: 1px solid var(--ta-border);
|
| 118 |
+
background: rgba(15, 17, 23, 0.7);
|
| 119 |
+
backdrop-filter: blur(10px);
|
| 120 |
+
z-index: 10;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.tb-title {
|
| 124 |
+
font-size: 18px;
|
| 125 |
+
font-weight: 600;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.tb-sub {
|
| 129 |
+
font-size: 12px;
|
| 130 |
+
color: var(--ta-text3);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.ta-scroll-content {
|
| 134 |
+
flex: 1;
|
| 135 |
+
overflow-y: auto;
|
| 136 |
+
padding: 24px;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* Metrics Grid */
|
| 140 |
+
.metrics-grid {
|
| 141 |
+
display: grid;
|
| 142 |
+
grid-template-columns: repeat(4, 1fr);
|
| 143 |
+
gap: 16px;
|
| 144 |
+
margin-bottom: 24px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.metric-card {
|
| 148 |
+
background: var(--ta-bg2);
|
| 149 |
+
border: 1px solid var(--ta-border);
|
| 150 |
+
border-radius: var(--ta-radius-lg);
|
| 151 |
+
padding: 16px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.mc-label {
|
| 155 |
+
font-size: 11px;
|
| 156 |
+
color: var(--ta-text3);
|
| 157 |
+
margin-bottom: 8px;
|
| 158 |
+
font-weight: 600;
|
| 159 |
+
text-transform: uppercase;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.mc-val {
|
| 163 |
+
font-size: 32px;
|
| 164 |
+
font-weight: 700;
|
| 165 |
+
line-height: 1;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.mc-sub {
|
| 169 |
+
font-size: 11px;
|
| 170 |
+
margin-top: 8px;
|
| 171 |
+
font-weight: 500;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.mc-up { color: var(--ta-accent); }
|
| 175 |
+
.mc-dn { color: var(--ta-red); }
|
| 176 |
+
|
| 177 |
+
/* Badges */
|
| 178 |
+
.ta-badge {
|
| 179 |
+
font-size: 10px;
|
| 180 |
+
padding: 2px 8px;
|
| 181 |
+
border-radius: 12px;
|
| 182 |
+
font-weight: 600;
|
| 183 |
+
display: inline-flex;
|
| 184 |
+
align-items: center;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.badge-red { background: var(--ta-red-bg); color: var(--ta-red); }
|
| 188 |
+
.badge-amber { background: var(--ta-amber-bg); color: var(--ta-amber); }
|
| 189 |
+
.badge-blue { background: var(--ta-blue-bg); color: var(--ta-blue); }
|
| 190 |
+
|
| 191 |
+
/* List Cards */
|
| 192 |
+
.ta-card {
|
| 193 |
+
background: var(--ta-bg2);
|
| 194 |
+
border: 1px solid var(--ta-border);
|
| 195 |
+
border-radius: var(--ta-radius-lg);
|
| 196 |
+
overflow: hidden;
|
| 197 |
+
margin-bottom: 20px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.card-head {
|
| 201 |
+
padding: 14px 20px;
|
| 202 |
+
border-bottom: 1px solid var(--ta-border);
|
| 203 |
+
display: flex;
|
| 204 |
+
align-items: center;
|
| 205 |
+
justify-content: space-between;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.ta-row {
|
| 209 |
+
display: flex;
|
| 210 |
+
align-items: center;
|
| 211 |
+
gap: 12px;
|
| 212 |
+
padding: 12px 20px;
|
| 213 |
+
border-bottom: 1px solid var(--ta-border);
|
| 214 |
+
transition: 0.15s;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.ta-row:hover {
|
| 218 |
+
background: var(--ta-bg3);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.ta-row:last-child { border-bottom: none; }
|
| 222 |
+
|
| 223 |
+
.student-av {
|
| 224 |
+
width: 40px;
|
| 225 |
+
height: 40px;
|
| 226 |
+
border-radius: 50%;
|
| 227 |
+
object-fit: cover;
|
| 228 |
+
border: 2px solid var(--ta-border);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.cell-info { flex: 1; }
|
| 232 |
+
.cell-name { font-size: 14px; font-weight: 600; }
|
| 233 |
+
.cell-sub { font-size: 12px; color: var(--ta-text3); }
|
| 234 |
+
|
| 235 |
+
/* Source Selector */
|
| 236 |
+
.source-grid {
|
| 237 |
+
display: flex;
|
| 238 |
+
gap: 10px;
|
| 239 |
+
flex-wrap: wrap;
|
| 240 |
+
margin-top: 12px;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.source-opt {
|
| 244 |
+
padding: 8px 16px;
|
| 245 |
+
border-radius: var(--ta-radius);
|
| 246 |
+
border: 1px solid var(--ta-border);
|
| 247 |
+
background: var(--ta-bg3);
|
| 248 |
+
font-size: 12px;
|
| 249 |
+
color: var(--ta-text2);
|
| 250 |
+
cursor: pointer;
|
| 251 |
+
display: flex;
|
| 252 |
+
align-items: center;
|
| 253 |
+
gap: 8px;
|
| 254 |
+
transition: 0.2s;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.source-opt.active {
|
| 258 |
+
border-color: var(--ta-accent);
|
| 259 |
+
background: var(--ta-accent-bg);
|
| 260 |
+
color: var(--ta-accent);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* Summary Content */
|
| 264 |
+
.summary-draft-body {
|
| 265 |
+
padding: 16px;
|
| 266 |
+
background: var(--ta-bg);
|
| 267 |
+
border-radius: 8px;
|
| 268 |
+
margin: 12px 0;
|
| 269 |
+
font-size: 13px;
|
| 270 |
+
line-height: 1.6;
|
| 271 |
+
color: var(--ta-text2);
|
| 272 |
+
white-space: pre-wrap;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.ta-btn {
|
| 276 |
+
padding: 8px 16px;
|
| 277 |
+
border-radius: var(--ta-radius);
|
| 278 |
+
border: 1px solid var(--ta-border);
|
| 279 |
+
background: var(--ta-bg3);
|
| 280 |
+
color: var(--ta-text);
|
| 281 |
+
font-size: 12px;
|
| 282 |
+
font-weight: 600;
|
| 283 |
+
cursor: pointer;
|
| 284 |
+
display: inline-flex;
|
| 285 |
+
align-items: center;
|
| 286 |
+
gap: 8px;
|
| 287 |
+
transition: 0.2s;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.ta-btn:hover {
|
| 291 |
+
border-color: var(--ta-border2);
|
| 292 |
+
color: var(--ta-text);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.ta-btn-primary {
|
| 296 |
+
background: var(--ta-accent);
|
| 297 |
+
color: #000;
|
| 298 |
+
border: none;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.ta-btn-primary:hover { background: var(--ta-accent2); }
|
| 302 |
+
|
| 303 |
+
.ta-btn-red { color: var(--ta-red); }
|
| 304 |
+
|
| 305 |
+
.spin {
|
| 306 |
+
animation: spin 1s linear infinite;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
@keyframes spin {
|
| 310 |
+
from { transform: rotate(0deg); }
|
| 311 |
+
to { transform: rotate(360deg); }
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.animate-fade {
|
| 315 |
+
animation: fadeIn 0.3s ease-out;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
@keyframes fadeIn {
|
| 319 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 320 |
+
to { opacity: 1; transform: translateY(0); }
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/* Empty State */
|
| 324 |
+
.empty-state {
|
| 325 |
+
display: flex;
|
| 326 |
+
flex-direction: column;
|
| 327 |
+
align-items: center;
|
| 328 |
+
justify-content: center;
|
| 329 |
+
padding: 60px 0;
|
| 330 |
+
color: var(--ta-text3);
|
| 331 |
+
text-align: center;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.empty-icon {
|
| 335 |
+
font-size: 40px;
|
| 336 |
+
margin-bottom: 16px;
|
| 337 |
+
opacity: 0.5;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/* --- Vibrant & Animated Styles --- */
|
| 341 |
+
|
| 342 |
+
@keyframes scan-line {
|
| 343 |
+
0% { top: 0%; opacity: 0; }
|
| 344 |
+
50% { opacity: 1; }
|
| 345 |
+
100% { top: 100%; opacity: 0; }
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
@keyframes brain-pulse {
|
| 349 |
+
0% { transform: scale(1); filter: drop-shadow(0 0 5px var(--ta-accent)); }
|
| 350 |
+
50% { transform: scale(1.1); filter: drop-shadow(0 0 15px var(--ta-accent)); }
|
| 351 |
+
100% { transform: scale(1); filter: drop-shadow(0 0 5px var(--ta-accent)); }
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.ai-processing-box {
|
| 355 |
+
position: relative;
|
| 356 |
+
overflow: hidden;
|
| 357 |
+
background: var(--ta-bg3);
|
| 358 |
+
border-radius: 16px;
|
| 359 |
+
padding: 40px;
|
| 360 |
+
text-align: center;
|
| 361 |
+
border: 1px solid var(--ta-accent-bg);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.scan-line {
|
| 365 |
+
position: absolute;
|
| 366 |
+
left: 0;
|
| 367 |
+
width: 100%;
|
| 368 |
+
height: 2px;
|
| 369 |
+
background: linear-gradient(90deg, transparent, var(--ta-accent), transparent);
|
| 370 |
+
animation: scan-line 2s infinite linear;
|
| 371 |
+
box-shadow: 0 0 10px var(--ta-accent);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.brain-icon {
|
| 375 |
+
font-size: 48px;
|
| 376 |
+
color: var(--ta-accent);
|
| 377 |
+
animation: brain-pulse 1.5s infinite ease-in-out;
|
| 378 |
+
margin-bottom: 20px;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.vibrant-btn {
|
| 382 |
+
background: linear-gradient(135deg, var(--ta-accent), #a855f7);
|
| 383 |
+
color: white !important;
|
| 384 |
+
border: none !important;
|
| 385 |
+
padding: 12px 24px !important;
|
| 386 |
+
font-weight: 600 !important;
|
| 387 |
+
border-radius: 8px !important;
|
| 388 |
+
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
|
| 389 |
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
| 390 |
+
display: flex;
|
| 391 |
+
align-items: center;
|
| 392 |
+
justify-content: center;
|
| 393 |
+
gap: 8px;
|
| 394 |
+
cursor: pointer;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.vibrant-btn:hover {
|
| 398 |
+
transform: translateY(-3px) scale(1.05);
|
| 399 |
+
box-shadow: 0 8px 25px rgba(124, 58, 237, 0.5);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.vibrant-btn:active {
|
| 403 |
+
transform: translateY(0) scale(0.95);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.time-selector {
|
| 407 |
+
display: flex;
|
| 408 |
+
background: var(--ta-bg3);
|
| 409 |
+
padding: 4px;
|
| 410 |
+
border-radius: 10px;
|
| 411 |
+
gap: 4px;
|
| 412 |
+
border: 1px solid var(--ta-border);
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.time-opt {
|
| 416 |
+
padding: 8px 16px;
|
| 417 |
+
border-radius: 8px;
|
| 418 |
+
cursor: pointer;
|
| 419 |
+
font-size: 12px;
|
| 420 |
+
font-weight: 500;
|
| 421 |
+
transition: 0.2s;
|
| 422 |
+
color: var(--ta-text3);
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.time-opt.active {
|
| 426 |
+
background: var(--ta-accent);
|
| 427 |
+
color: white;
|
| 428 |
+
box-shadow: 0 2px 8px rgba(124, 58, 237, 0.2);
|
| 429 |
+
}
|
src/pages/TADashboard.jsx
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import { useSelector } from 'react-redux';
|
| 3 |
+
import {
|
| 4 |
+
FiAlertCircle, FiCheckCircle, FiMessageSquare, FiRefreshCw,
|
| 5 |
+
FiTrash2, FiEdit, FiClock, FiUsers, FiFileText, FiCalendar,
|
| 6 |
+
FiSettings, FiShare2, FiMoreVertical, FiTrendingUp, FiActivity, FiCpu, FiInfo, FiUploadCloud, FiSend, FiChevronRight
|
| 7 |
+
} from 'react-icons/fi';
|
| 8 |
+
import taService from '../services/ta.service';
|
| 9 |
+
import './TADashboard.css';
|
| 10 |
+
|
| 11 |
+
const TADashboard = () => {
|
| 12 |
+
const { activeSpace } = useSelector((state) => state.app);
|
| 13 |
+
const [atRiskList, setAtRiskList] = useState([]);
|
| 14 |
+
const [summaryQueue, setSummaryQueue] = useState([]);
|
| 15 |
+
const [actionLogs, setActionLogs] = useState([]);
|
| 16 |
+
const [loading, setLoading] = useState(false);
|
| 17 |
+
const [uploading, setUploading] = useState(false);
|
| 18 |
+
const [activeTab, setActiveTab] = useState('at-risk');
|
| 19 |
+
const [aiContext, setAiContext] = useState(null);
|
| 20 |
+
const [uploadedFile, setUploadedFile] = useState(null);
|
| 21 |
+
|
| 22 |
+
// AI Flow State
|
| 23 |
+
const [currentStep, setCurrentStep] = useState(1);
|
| 24 |
+
const [aiPreview, setAiPreview] = useState(null);
|
| 25 |
+
const [sendTime, setSendTime] = useState('now');
|
| 26 |
+
const [scheduleDate, setScheduleDate] = useState('');
|
| 27 |
+
|
| 28 |
+
const fetchData = async () => {
|
| 29 |
+
if (!activeSpace) return;
|
| 30 |
+
setLoading(true);
|
| 31 |
+
try {
|
| 32 |
+
const [atRiskRes, queueRes, logsRes] = await Promise.allSettled([
|
| 33 |
+
taService.getAtRiskList(activeSpace),
|
| 34 |
+
taService.getSummaryQueue(activeSpace),
|
| 35 |
+
taService.getActionLogs(activeSpace)
|
| 36 |
+
]);
|
| 37 |
+
|
| 38 |
+
if (atRiskRes.status === 'fulfilled') setAtRiskList(atRiskRes.value.data || []);
|
| 39 |
+
if (queueRes.status === 'fulfilled') setSummaryQueue(queueRes.value.data || []);
|
| 40 |
+
if (logsRes.status === 'fulfilled') setActionLogs(logsRes.value.data || []);
|
| 41 |
+
} catch (error) {
|
| 42 |
+
console.error('Failed to fetch TA Dashboard data:', error);
|
| 43 |
+
} finally {
|
| 44 |
+
setLoading(false);
|
| 45 |
+
}
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
useEffect(() => {
|
| 49 |
+
fetchData();
|
| 50 |
+
const interval = setInterval(fetchData, 60000);
|
| 51 |
+
return () => clearInterval(interval);
|
| 52 |
+
}, [activeSpace]);
|
| 53 |
+
|
| 54 |
+
const handleFileUpload = async (e) => {
|
| 55 |
+
const file = e.target.files[0];
|
| 56 |
+
if (!file) return;
|
| 57 |
+
|
| 58 |
+
setUploading(true);
|
| 59 |
+
try {
|
| 60 |
+
const res = await taService.uploadSlide(activeSpace, file);
|
| 61 |
+
setUploadedFile(res.data);
|
| 62 |
+
} catch (error) {
|
| 63 |
+
console.error('Upload failed:', error);
|
| 64 |
+
} finally {
|
| 65 |
+
setUploading(false);
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
const startAiAnalysis = () => {
|
| 70 |
+
setCurrentStep(2);
|
| 71 |
+
setTimeout(() => {
|
| 72 |
+
setAiPreview({
|
| 73 |
+
content: `Tóm tắt buổi học hôm nay:\n\n1. Chúng ta đã học về React Hooks nâng cao (useMemo, useCallback).\n2. Cách tối ưu hiệu năng và các lỗi thường gặp khi sử dụng useEffect.\n3. Thảo luận về kiến trúc Atomic Design trong việc chia component.\n\n📌 Bài tập về nhà: Hoàn thiện Dashboard cho dự án cá nhân và nộp trước 23h ngày mai.`,
|
| 74 |
+
source: uploadedFile?.filename
|
| 75 |
+
});
|
| 76 |
+
setCurrentStep(3);
|
| 77 |
+
}, 3000);
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
const handleResolveAlert = async (id) => {
|
| 81 |
+
try {
|
| 82 |
+
await taService.resolveAlert(id, activeSpace);
|
| 83 |
+
await fetchData();
|
| 84 |
+
} catch (error) {
|
| 85 |
+
console.error('Failed to resolve alert:', error);
|
| 86 |
+
}
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
const handleApproveSummary = async () => {
|
| 90 |
+
try {
|
| 91 |
+
await fetchData();
|
| 92 |
+
setCurrentStep(1);
|
| 93 |
+
setAiPreview(null);
|
| 94 |
+
setUploadedFile(null);
|
| 95 |
+
alert(sendTime === 'now' ? 'Bản tóm tắt đã được đăng!' : `Đã hẹn lịch gửi vào lúc: ${scheduleDate}`);
|
| 96 |
+
} catch (error) {
|
| 97 |
+
console.error('Failed to approve summary:', error);
|
| 98 |
+
}
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
const handleScanAtRisk = async () => {
|
| 102 |
+
setLoading(true);
|
| 103 |
+
try {
|
| 104 |
+
await taService.scanAtRisk(activeSpace);
|
| 105 |
+
await fetchData();
|
| 106 |
+
} catch (error) {
|
| 107 |
+
console.error('Failed to scan at-risk:', error);
|
| 108 |
+
} finally {
|
| 109 |
+
setLoading(false);
|
| 110 |
+
}
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
const handleGetAiContext = async (snapshotId) => {
|
| 114 |
+
try {
|
| 115 |
+
const res = await taService.getAtRiskContext(snapshotId, activeSpace);
|
| 116 |
+
setAiContext({ id: snapshotId, ...res.data });
|
| 117 |
+
} catch (error) {
|
| 118 |
+
console.error('Failed to get AI Context:', error);
|
| 119 |
+
}
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
const criticalCount = atRiskList.filter(s => s.level === 'critical').length;
|
| 123 |
+
const warningCount = atRiskList.filter(s => s.level === 'warning').length;
|
| 124 |
+
|
| 125 |
+
return (
|
| 126 |
+
<div className="ta-dashboard-container">
|
| 127 |
+
{/* Internal Sidebar */}
|
| 128 |
+
<aside className="ta-internal-sidebar">
|
| 129 |
+
<div className="sb-head">
|
| 130 |
+
<div className="sb-title">Quản lý TA</div>
|
| 131 |
+
<div className="sb-sub">{activeSpace?.substring(0, 8)}...</div>
|
| 132 |
+
</div>
|
| 133 |
+
<div className="sb-section">Chức năng chính</div>
|
| 134 |
+
<div className={`sb-item ${activeTab === 'at-risk' ? 'active' : ''}`} onClick={() => setActiveTab('at-risk')}>
|
| 135 |
+
<div className="sb-icon" style={{background: 'var(--ta-red-bg)', color: 'var(--ta-red)'}}><FiAlertCircle /></div>
|
| 136 |
+
<span>At-Risk Alert</span>
|
| 137 |
+
{atRiskList.length > 0 && <span className="ta-badge badge-red" style={{marginLeft: 'auto'}}>{atRiskList.length}</span>}
|
| 138 |
+
</div>
|
| 139 |
+
<div className={`sb-item ${activeTab === 'summary' ? 'active' : ''}`} onClick={() => setActiveTab('summary')}>
|
| 140 |
+
<div className="sb-icon" style={{background: 'var(--ta-accent-bg)', color: 'var(--ta-accent)'}}><FiFileText /></div>
|
| 141 |
+
<span>AI Summary Queue</span>
|
| 142 |
+
{summaryQueue.length > 0 && <span className="ta-badge badge-amber" style={{marginLeft: 'auto'}}>{summaryQueue.length}</span>}
|
| 143 |
+
</div>
|
| 144 |
+
<div className={`sb-item ${activeTab === 'logs' ? 'active' : ''}`} onClick={() => setActiveTab('logs')}>
|
| 145 |
+
<div className="sb-icon" style={{background: 'var(--ta-blue-bg)', color: 'var(--ta-blue)'}}><FiActivity /></div>
|
| 146 |
+
<span>Nhật ký hành động</span>
|
| 147 |
+
</div>
|
| 148 |
+
<div className="sb-section">Cài đặt</div>
|
| 149 |
+
<div className="sb-item">
|
| 150 |
+
<div className="sb-icon"><FiSettings /></div>
|
| 151 |
+
<span>Cấu hình AI</span>
|
| 152 |
+
</div>
|
| 153 |
+
</aside>
|
| 154 |
+
|
| 155 |
+
{/* Main Content Area */}
|
| 156 |
+
<div className="ta-main-content">
|
| 157 |
+
<header className="ta-topbar">
|
| 158 |
+
<div>
|
| 159 |
+
<div className="tb-title">
|
| 160 |
+
{activeTab === 'at-risk' ? 'Học viên cần quan tâm' : activeTab === 'summary' ? 'Quy trình Tóm tắt buổi học' : 'Nhật ký hoạt động TA'}
|
| 161 |
+
</div>
|
| 162 |
+
<div className="tb-sub">
|
| 163 |
+
{activeTab === 'at-risk' ? `${atRiskList.length} học viên cần hành động` : activeTab === 'summary' ? 'Tạo bản tóm tắt thông minh dựa trên tài liệu' : 'Lịch sử thao tác gần nhất'}
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
<div className="dashboard-actions" style={{display: 'flex', gap: '8px'}}>
|
| 167 |
+
<button className="ta-btn" style={{height: '38px'}} onClick={fetchData} disabled={loading}>
|
| 168 |
+
<FiRefreshCw className={loading ? 'spin' : ''} /> Làm mới
|
| 169 |
+
</button>
|
| 170 |
+
{activeTab === 'at-risk' && (
|
| 171 |
+
<button className="vibrant-btn" style={{height: '38px', padding: '0 20px', fontSize: '13px'}} onClick={handleScanAtRisk} disabled={loading}>
|
| 172 |
+
<FiTrendingUp /> Quét học viên
|
| 173 |
+
</button>
|
| 174 |
+
)}
|
| 175 |
+
</div>
|
| 176 |
+
</header>
|
| 177 |
+
|
| 178 |
+
<div className="ta-scroll-content">
|
| 179 |
+
{activeTab === 'at-risk' && (
|
| 180 |
+
<div className="metrics-grid animate-fade">
|
| 181 |
+
<div className="metric-card">
|
| 182 |
+
<div className="mc-label">Nguy cấp</div>
|
| 183 |
+
<div className="mc-val" style={{color: 'var(--ta-red)'}}>{criticalCount}</div>
|
| 184 |
+
<div className="mc-sub mc-dn">Cần xử lý ngay</div>
|
| 185 |
+
</div>
|
| 186 |
+
<div className="metric-card">
|
| 187 |
+
<div className="mc-label">Cảnh báo</div>
|
| 188 |
+
<div className="mc-val" style={{color: 'var(--ta-amber)'}}>{warningCount}</div>
|
| 189 |
+
<div className="mc-sub">Đang theo dõi</div>
|
| 190 |
+
</div>
|
| 191 |
+
<div className="metric-card">
|
| 192 |
+
<div className="mc-label">Tỷ lệ xử lý</div>
|
| 193 |
+
<div className="mc-val">92%</div>
|
| 194 |
+
<div className="mc-sub mc-up">↑ 4% tuần này</div>
|
| 195 |
+
</div>
|
| 196 |
+
<div className="metric-card">
|
| 197 |
+
<div className="mc-label">Avg Respond</div>
|
| 198 |
+
<div className="mc-val">15p</div>
|
| 199 |
+
<div className="mc-sub mc-up">Tốt hơn 20%</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
)}
|
| 203 |
+
|
| 204 |
+
{activeTab === 'at-risk' ? (
|
| 205 |
+
<div className="animate-fade">
|
| 206 |
+
<div className="ta-card">
|
| 207 |
+
<div className="card-head">
|
| 208 |
+
<span style={{fontWeight: 600}}>🔴 Học viên có dấu hiệu rủi ro</span>
|
| 209 |
+
<span className="ta-badge badge-red">{atRiskList.length}</span>
|
| 210 |
+
</div>
|
| 211 |
+
{atRiskList.length === 0 ? (
|
| 212 |
+
<div className="empty-state">
|
| 213 |
+
<FiCheckCircle className="empty-icon" style={{color: 'var(--ta-accent)'}} />
|
| 214 |
+
<p>Lớp học hiện tại rất ổn định.</p>
|
| 215 |
+
</div>
|
| 216 |
+
) : (
|
| 217 |
+
atRiskList.map(student => (
|
| 218 |
+
<div key={student.id} style={{borderBottom: '1px solid var(--ta-border)'}}>
|
| 219 |
+
<div className="ta-row">
|
| 220 |
+
<img src={student.profiles?.avatar_url || 'https://ui-avatars.com/api/?name=' + (student.profiles?.display_name || 'Student')} className="student-av" alt={student.profiles?.display_name} />
|
| 221 |
+
<div className="cell-info">
|
| 222 |
+
<div className="cell-name">{student.profiles?.display_name || 'Học viên ẩn danh'}</div>
|
| 223 |
+
<div className="cell-sub">{student.reason} · {new Date(student.created_at).toLocaleTimeString('vi-VN')}</div>
|
| 224 |
+
</div>
|
| 225 |
+
<div className={`ta-badge ${student.level === 'critical' ? 'badge-red' : 'badge-amber'}`}>
|
| 226 |
+
{student.level === 'critical' ? 'Critical' : 'Warning'}
|
| 227 |
+
</div>
|
| 228 |
+
<button className="ta-btn" style={{borderColor: 'var(--ta-accent)', color: 'var(--ta-accent)'}} onClick={() => handleGetAiContext(student.id)}>
|
| 229 |
+
<FiMessageSquare /> Nhắn tin
|
| 230 |
+
</button>
|
| 231 |
+
<button className="ta-btn" onClick={() => handleResolveAlert(student.id)}>
|
| 232 |
+
<FiCheckCircle /> Đã xử lý
|
| 233 |
+
</button>
|
| 234 |
+
</div>
|
| 235 |
+
{aiContext?.id === student.id && (
|
| 236 |
+
<div style={{padding: '0 20px 16px 72px'}} className="animate-fade">
|
| 237 |
+
<div style={{background: 'var(--ta-bg3)', padding: '16px', borderRadius: '12px', border: '1px solid var(--ta-accent-bg)', display: 'flex', alignItems: 'center', gap: '12px'}}>
|
| 238 |
+
<div className="sb-icon" style={{background: 'var(--ta-accent-bg)', color: 'var(--ta-accent)', width: '32px', height: '32px'}}><FiCpu /></div>
|
| 239 |
+
<div style={{flex: 1}}>
|
| 240 |
+
<div style={{fontSize: '12px', fontWeight: 600, color: 'var(--ta-text)'}}>Đã chuẩn bị bộ Context cho học viên {student.profiles?.display_name}</div>
|
| 241 |
+
<div style={{fontSize: '11px', color: 'var(--ta-text3)', marginTop: '2px'}}>Dữ liệu đã được lưu trữ an toàn. Agent sẽ tự động lấy thông tin này để soạn tin nhắn.</div>
|
| 242 |
+
</div>
|
| 243 |
+
<button className="ta-btn" style={{fontSize: '11px'}} onClick={() => setAiContext(null)}>Đóng</button>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
)}
|
| 247 |
+
</div>
|
| 248 |
+
))
|
| 249 |
+
)}
|
| 250 |
+
</div>
|
| 251 |
+
</div>
|
| 252 |
+
) : activeTab === 'summary' ? (
|
| 253 |
+
<div className="animate-fade">
|
| 254 |
+
{/* Step Flow Header */}
|
| 255 |
+
<div className="ta-card" style={{marginBottom: '20px'}}>
|
| 256 |
+
<div style={{display: 'flex', borderBottom: '1px solid var(--ta-border)'}}>
|
| 257 |
+
{['1. Tài liệu', '2. AI Phân tích', '3. Duyệt & Đăng'].map((step, idx) => (
|
| 258 |
+
<div key={idx} style={{
|
| 259 |
+
flex: 1, padding: '16px', textAlign: 'center', fontSize: '12px',
|
| 260 |
+
color: currentStep >= idx + 1 ? 'var(--ta-accent)' : 'var(--ta-text3)',
|
| 261 |
+
borderBottom: currentStep === idx + 1 ? '2px solid var(--ta-accent)' : 'none',
|
| 262 |
+
fontWeight: currentStep === idx + 1 ? 700 : 500, transition: '0.3s'
|
| 263 |
+
}}>
|
| 264 |
+
{currentStep > idx + 1 ? `✓ ${step}` : step}
|
| 265 |
+
</div>
|
| 266 |
+
))}
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<div style={{padding: '30px'}}>
|
| 270 |
+
{/* STEP 1: UPLOAD */}
|
| 271 |
+
{currentStep === 1 && (
|
| 272 |
+
<div className="animate-fade">
|
| 273 |
+
<div
|
| 274 |
+
className="upload-zone"
|
| 275 |
+
style={{border: '2px dashed var(--ta-border2)', background: uploadedFile ? 'var(--ta-bg-success)' : 'var(--ta-bg2)', height: '180px', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center'}}
|
| 276 |
+
onClick={() => document.getElementById('slide-upload').click()}
|
| 277 |
+
>
|
| 278 |
+
{uploading ? <FiRefreshCw className="spin" size={32} /> :
|
| 279 |
+
uploadedFile ? <FiCheckCircle size={32} style={{color: 'var(--ta-accent)'}} /> :
|
| 280 |
+
<FiUploadCloud size={32} style={{color: 'var(--ta-accent)', marginBottom: '12px'}} />}
|
| 281 |
+
|
| 282 |
+
<div style={{fontWeight: 600, fontSize: '15px', marginTop: '10px'}}>
|
| 283 |
+
{uploadedFile ? uploadedFile.filename : 'Tải lên Slide bài giảng (PDF, IMG)'}
|
| 284 |
+
</div>
|
| 285 |
+
<div style={{fontSize: '12px', color: 'var(--ta-text3)', marginTop: '4px'}}>
|
| 286 |
+
{uploadedFile ? `${(uploadedFile.size/1024).toFixed(1)} KB - Đã sẵn sàng` : 'Hệ thống sẽ kết hợp Slide và Chat Log để tóm tắt'}
|
| 287 |
+
</div>
|
| 288 |
+
<input type="file" style={{display: 'none'}} id="slide-upload" accept=".pdf,image/*" onChange={handleFileUpload} />
|
| 289 |
+
</div>
|
| 290 |
+
|
| 291 |
+
<div style={{marginTop: '30px', textAlign: 'center'}}>
|
| 292 |
+
<button
|
| 293 |
+
className="vibrant-btn"
|
| 294 |
+
style={{width: '240px', height: '46px', fontSize: '14px'}}
|
| 295 |
+
onClick={startAiAnalysis}
|
| 296 |
+
disabled={!uploadedFile}
|
| 297 |
+
>
|
| 298 |
+
Bắt đầu Phân tích <FiChevronRight />
|
| 299 |
+
</button>
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
)}
|
| 303 |
+
|
| 304 |
+
{/* STEP 2: PROCESSING */}
|
| 305 |
+
{currentStep === 2 && (
|
| 306 |
+
<div className="ai-processing-box animate-fade">
|
| 307 |
+
<div className="scan-line"></div>
|
| 308 |
+
<div className="brain-icon"><FiCpu /></div>
|
| 309 |
+
<div style={{fontWeight: 700, fontSize: '18px', color: 'var(--ta-text)', marginBottom: '8px'}}>AI ĐANG TỔNG HỢP...</div>
|
| 310 |
+
<div style={{fontSize: '13px', color: 'var(--ta-text3)', maxWidth: '400px', margin: '0 auto'}}>
|
| 311 |
+
Đang đọc tài liệu {uploadedFile?.filename} và quét nội dung thảo luận trong lớp học để soạn bản thảo tóm tắt.
|
| 312 |
+
</div>
|
| 313 |
+
</div>
|
| 314 |
+
)}
|
| 315 |
+
|
| 316 |
+
{/* STEP 3: PREVIEW & SCHEDULE */}
|
| 317 |
+
{currentStep === 3 && (
|
| 318 |
+
<div className="animate-fade">
|
| 319 |
+
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px'}}>
|
| 320 |
+
<div style={{fontSize: '13px', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--ta-accent)'}}>
|
| 321 |
+
<FiCpu /> BẢN NHÁP TÓM TẮT THÔNG MINH
|
| 322 |
+
</div>
|
| 323 |
+
<div style={{fontSize: '11px', color: 'var(--ta-text3)'}}>Học liệu: {uploadedFile?.filename}</div>
|
| 324 |
+
</div>
|
| 325 |
+
|
| 326 |
+
<textarea
|
| 327 |
+
className="ta-textarea"
|
| 328 |
+
value={aiPreview.content}
|
| 329 |
+
onChange={(e) => setAiPreview({...aiPreview, content: e.target.value})}
|
| 330 |
+
style={{width: '100%', height: '180px', background: 'var(--ta-bg)', border: '1px solid var(--ta-border)', borderRadius: '12px', padding: '16px', fontSize: '14px', color: 'var(--ta-text2)', lineHeight: 1.6, outline: 'none'}}
|
| 331 |
+
/>
|
| 332 |
+
|
| 333 |
+
<div style={{marginTop: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', background: 'var(--ta-bg2)', padding: '20px', borderRadius: '12px', gap: '20px'}}>
|
| 334 |
+
<div style={{flex: 1}}>
|
| 335 |
+
<div style={{fontSize: '11px', fontWeight: 700, color: 'var(--ta-text3)', marginBottom: '10px', textTransform: 'uppercase'}}>Lựa chọn thời gian gửi</div>
|
| 336 |
+
<div className="time-selector" style={{width: 'fit-content'}}>
|
| 337 |
+
<div className={`time-opt ${sendTime === 'now' ? 'active' : ''}`} onClick={() => setSendTime('now')}>
|
| 338 |
+
<FiSend /> Gửi ngay
|
| 339 |
+
</div>
|
| 340 |
+
<div className={`time-opt ${sendTime === 'schedule' ? 'active' : ''}`} onClick={() => setSendTime('schedule')}>
|
| 341 |
+
<FiClock /> Hẹn giờ
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
|
| 345 |
+
{sendTime === 'schedule' && (
|
| 346 |
+
<div className="animate-fade" style={{marginTop: '12px'}}>
|
| 347 |
+
<input
|
| 348 |
+
type="datetime-local"
|
| 349 |
+
className="ta-input"
|
| 350 |
+
style={{width: '200px', padding: '8px', borderRadius: '6px', border: '1px solid var(--ta-border)', background: 'var(--ta-bg)', color: 'var(--ta-text)'}}
|
| 351 |
+
value={scheduleDate}
|
| 352 |
+
onChange={(e) => setScheduleDate(e.target.value)}
|
| 353 |
+
/>
|
| 354 |
+
</div>
|
| 355 |
+
)}
|
| 356 |
+
</div>
|
| 357 |
+
|
| 358 |
+
<div style={{display: 'flex', gap: '10px', marginTop: '20px'}}>
|
| 359 |
+
<button className="ta-btn" style={{height: '42px', padding: '0 20px'}} onClick={() => setCurrentStep(1)}>
|
| 360 |
+
<FiRefreshCw /> Làm lại
|
| 361 |
+
</button>
|
| 362 |
+
<button className="vibrant-btn" style={{height: '42px', minWidth: '180px'}} onClick={handleApproveSummary}>
|
| 363 |
+
<FiCheckCircle /> {sendTime === 'now' ? 'Duyệt & Đăng bài' : 'Xác nhận đặt lịch'}
|
| 364 |
+
</button>
|
| 365 |
+
</div>
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
)}
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
+
</div>
|
| 372 |
+
) : (
|
| 373 |
+
<div className="animate-fade">
|
| 374 |
+
{/* Logs Tab */}
|
| 375 |
+
<div className="ta-card">
|
| 376 |
+
<div className="card-head"><span style={{fontWeight: 600}}>📜 Lịch sử hành động</span></div>
|
| 377 |
+
{actionLogs.length === 0 ? (
|
| 378 |
+
<div className="empty-state"><p>Chưa có hành động nào.</p></div>
|
| 379 |
+
) : (
|
| 380 |
+
actionLogs.map(log => (
|
| 381 |
+
<div key={log.id} className="ta-row">
|
| 382 |
+
<div className="sb-icon" style={{background: 'var(--ta-bg4)'}}>{log.action_type === 'dismissed_alert' ? '✅' : log.action_type === 'upload_document' ? '📁' : '📝'}</div>
|
| 383 |
+
<div className="cell-info">
|
| 384 |
+
<div className="cell-name"><strong>{log.ta?.display_name || 'TA'}</strong> {log.action_type === 'dismissed_alert' ? 'đã xử lý cảnh báo cho' : log.action_type === 'upload_document' ? 'đã tải lên' : 'đã duyệt tóm tắt'}</div>
|
| 385 |
+
<div className="cell-sub">{log.notes}</div>
|
| 386 |
+
</div>
|
| 387 |
+
<div style={{fontSize: '11px', color: 'var(--ta-text3)'}}>{new Date(log.created_at).toLocaleString('vi-VN')}</div>
|
| 388 |
+
</div>
|
| 389 |
+
))
|
| 390 |
+
)}
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
| 393 |
+
)}
|
| 394 |
+
</div>
|
| 395 |
+
</div>
|
| 396 |
+
</div>
|
| 397 |
+
);
|
| 398 |
+
};
|
| 399 |
+
|
| 400 |
+
export default TADashboard;
|
src/store/slices/appSlice.js
CHANGED
|
@@ -54,6 +54,10 @@ const appSlice = createSlice({
|
|
| 54 |
cancelCreateSpace: (state) => {
|
| 55 |
state.activeView = "space";
|
| 56 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
// 🆕 App loading state reducers
|
| 58 |
setAppLoading: (state, action) => {
|
| 59 |
state.appLoading = action.payload;
|
|
@@ -84,6 +88,7 @@ export const {
|
|
| 84 |
navigateToMessages,
|
| 85 |
openCreateSpace,
|
| 86 |
cancelCreateSpace,
|
|
|
|
| 87 |
// 🆕 App loading exports
|
| 88 |
setAppLoading,
|
| 89 |
setAppLoadingPhase,
|
|
|
|
| 54 |
cancelCreateSpace: (state) => {
|
| 55 |
state.activeView = "space";
|
| 56 |
},
|
| 57 |
+
navigateToDashboard: (state) => {
|
| 58 |
+
state.activeView = "dashboard";
|
| 59 |
+
state.isSettings = false;
|
| 60 |
+
},
|
| 61 |
// 🆕 App loading state reducers
|
| 62 |
setAppLoading: (state, action) => {
|
| 63 |
state.appLoading = action.payload;
|
|
|
|
| 88 |
navigateToMessages,
|
| 89 |
openCreateSpace,
|
| 90 |
cancelCreateSpace,
|
| 91 |
+
navigateToDashboard,
|
| 92 |
// 🆕 App loading exports
|
| 93 |
setAppLoading,
|
| 94 |
setAppLoadingPhase,
|