quachtiensinh27 commited on
Commit
477a7c3
·
1 Parent(s): 57f5158

cập nhật ui dashboard

Browse files
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,