source: pages/dashboard/admin/+Page.tsx@ d07d68c

main
Last change on this file since d07d68c was d07d68c, checked in by Mihail <mihail2.naumov@…>, 5 months ago

Fixed clone auth errors and component names in the component dialogs.

  • Property mode set to 100644
File size: 25.7 KB
Line 
1import React, {useEffect, useState} from 'react';
2import {
3 Container, Paper, Typography, Box, Avatar, Divider, Button,
4 CircularProgress, Table, TableBody, TableCell, TableHead, TableRow,
5 Chip, IconButton, TextField, Dialog, DialogTitle, DialogContent, DialogActions
6} from '@mui/material';
7import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
8import CheckCircleIcon from '@mui/icons-material/CheckCircle';
9import CancelIcon from '@mui/icons-material/Cancel';
10import BuildIcon from '@mui/icons-material/Build';
11import MemoryIcon from '@mui/icons-material/Memory';
12import AddIcon from '@mui/icons-material/Add';
13import DeleteIcon from '@mui/icons-material/Delete';
14
15import {
16 getAdminInfoAndData,
17 onSetBuildApprovalStatus,
18 onSetComponentSuggestionStatus
19} from './adminDashboard.telefunc';
20
21import BuildCard from '../../../components/BuildCard';
22import BuildDetailsDialog from '../../../components/BuildDetailsDialog';
23import {onDeleteBuild} from "../user/userDashboard.telefunc";
24import AddComponentDialog from "../../../components/AddComponentDialog";
25
26export default function AdminDashboard() {
27 const [data, setData] = useState<any>(null);
28 const [loading, setLoading] = useState(true);
29 const [selectedBuildId, setSelectedBuildId] = useState<number | null>(null);
30 const [deleteDialog, setDeleteDialog] = useState<{
31 open: boolean,
32 buildId: number | null,
33 buildName: string
34 }>({open: false, buildId: null, buildName: ''});
35 const [deleteLoading, setDeleteLoading] = useState(false);
36 const [addDialogOpen, setAddDialogOpen] = useState(false);
37
38 const [buildApprovalDialog, setBuildApprovalDialog] = useState<{
39 open: boolean,
40 buildId: number | null,
41 buildName: string
42 }>({open: false, buildId: null, buildName: ''});
43 const [rejectReason, setRejectReason] = useState('');
44 const [approvalLoading, setApprovalLoading] = useState(false);
45
46 const [suggestionDialog, setSuggestionDialog] = useState<{
47 open: boolean,
48 id: number | null,
49 action: 'approved' | 'rejected'
50 }>({open: false, id: null, action: 'approved'});
51 const [adminComment, setAdminComment] = useState("");
52
53 const [createComponentDialog, setCreateComponentDialog] = useState<{
54 open: boolean;
55 suggestion: any | null;
56 }>({open: false, suggestion: null});
57
58 const loadData = () => {
59 setLoading(true);
60 getAdminInfoAndData()
61 .then(res => {
62 setData(res);
63 setLoading(false);
64 })
65 .catch(err => {
66 console.error(err);
67 alert("Failed to load admin data. Are you an admin?");
68 setLoading(false);
69 });
70 };
71
72 useEffect(() => {
73 loadData();
74 }, []);
75
76 const openBuildApproval = (buildId: number, buildName: string) => {
77 setBuildApprovalDialog({open: true, buildId, buildName});
78 setRejectReason('');
79 };
80
81 const handleApproveBuild = async () => {
82 if (!buildApprovalDialog.buildId) return;
83 setApprovalLoading(true);
84 try {
85 await onSetBuildApprovalStatus({buildId: buildApprovalDialog.buildId, isApproved: true});
86 setBuildApprovalDialog({open: false, buildId: null, buildName: ''});
87 loadData();
88 } catch (e) {
89 alert("Approve failed");
90 } finally {
91 setApprovalLoading(false);
92 }
93 };
94
95 const handleRejectBuild = async () => {
96 if (!buildApprovalDialog.buildId || !rejectReason.trim()) return;
97 setApprovalLoading(true);
98 try {
99 await onSetBuildApprovalStatus({
100 buildId: buildApprovalDialog.buildId,
101 isApproved: false,
102 });
103 setBuildApprovalDialog({open: false, buildId: null, buildName: ''});
104 loadData();
105 } catch (e) {
106 console.error("Reject error:", e); // 🔧 add this
107 alert("Reject failed");
108 } finally {
109 setApprovalLoading(false);
110 }
111 };
112
113 const openSuggestionReview = (id: number, action: 'approved' | 'rejected') => {
114 setSuggestionDialog({open: true, id, action});
115 setAdminComment(action === 'approved' ? "" : "");
116 };
117
118 const submitSuggestionReview = async () => {
119 if (!suggestionDialog.id) return;
120 try {
121 await onSetComponentSuggestionStatus({
122 suggestionId: suggestionDialog.id,
123 status: suggestionDialog.action,
124 adminComment: adminComment
125 });
126 setSuggestionDialog({...suggestionDialog, open: false});
127 loadData();
128 } catch (e) {
129 alert("Failed to update suggestion");
130 }
131 };
132
133 const openDeleteDialog = (buildId: number, buildName: string) => {
134 setDeleteDialog({open: true, buildId, buildName});
135 };
136
137 const handleDelete = async () => {
138 if (!deleteDialog.buildId) return;
139 setDeleteLoading(true);
140 try {
141 await onDeleteBuild({buildId: deleteDialog.buildId});
142 setDeleteDialog({open: false, buildId: null, buildName: ''});
143 loadData();
144 setSelectedBuildId(null);
145 } catch (e) {
146 alert("Failed to delete build");
147 } finally {
148 setDeleteLoading(false);
149 }
150 };
151
152 if (loading) {
153 return (
154 <Container maxWidth="xl" sx={{
155 mt: 4,
156 mb: 10,
157 display: 'flex',
158 justifyContent: 'center',
159 alignItems: 'center',
160 minHeight: '50vh'
161 }}>
162 <CircularProgress/>
163 </Container>
164 );
165 }
166
167 if (!data) {
168 return (
169 <Container maxWidth="xl" sx={{mt: 4, mb: 10, textAlign: 'center', p: 5}}>
170 <Typography variant="h6" color="error">
171 Access Denied - Admin privileges required
172 </Typography>
173 </Container>
174 );
175 }
176
177 return (
178 <Container maxWidth="xl" sx={{mt: 4, mb: 10}}>
179 <Box sx={{
180 display: 'grid',
181 gridTemplateColumns: {
182 xs: '1fr',
183 md: '300px 1fr'
184 },
185 gap: 4
186 }}>
187 <Box>
188 <Paper elevation={3} sx={{
189 p: 4,
190 display: 'flex',
191 flexDirection: 'column',
192 alignItems: 'center',
193 height: '100%',
194 bgcolor: '#1e1e1e',
195 color: 'white'
196 }}>
197 <Avatar sx={{width: 120, height: 120, mb: 2, bgcolor: 'error.main'}}>
198 <AdminPanelSettingsIcon sx={{fontSize: 70}}/>
199 </Avatar>
200 <Typography variant="h5" fontWeight="bold">{data.admin?.username || 'Admin'}</Typography>
201 <Typography variant="body2" sx={{opacity: 0.7, mb: 3}}>
202 {data.admin?.email || 'admin@example.com'}
203 </Typography>
204
205 <Chip label="ADMINISTRATOR" color="error" variant="outlined" sx={{fontWeight: 'bold'}}/>
206 <Divider sx={{width: '100%', my: 3, bgcolor: 'rgba(255,255,255,0.1)'}}/>
207
208 <Box sx={{width: '100%', textAlign: 'center'}}>
209 <Typography variant="h3" fontWeight="bold" color="primary.main">
210 {data.pendingBuilds?.length || 0}
211 </Typography>
212 <Typography variant="caption">Pending Builds</Typography>
213 </Box>
214
215 <Box sx={{width: '100%', textAlign: 'center', mt: 3}}>
216 <Button
217 variant="contained"
218 color="warning"
219 size="large"
220 fullWidth
221 startIcon={<BuildIcon/>}
222 onClick={() => setAddDialogOpen(true)}
223 sx={{mb: 2}}
224 >
225 Add Component
226 </Button>
227 </Box>
228 </Paper>
229 </Box>
230
231 <Box sx={{
232 display: 'flex',
233 flexDirection: 'column',
234 gap: 4
235 }}>
236 <Paper sx={{p: 3, borderLeft: '6px solid #9c27b0'}}>
237 <Box sx={{display: 'flex', alignItems: 'center', mb: 2}}>
238 <MemoryIcon color="secondary" sx={{mr: 1}}/>
239 <Typography variant="h6" fontWeight="bold">
240 Component Suggestions ({data.componentSuggestions?.length || 0})
241 </Typography>
242 </Box>
243
244 {data.componentSuggestions?.length === 0 ? (
245 <Typography color="text.secondary">No suggestions.</Typography>
246 ) : (
247 <Box sx={{width: '100%', overflowX: 'auto'}}>
248 <Table size="small">
249 <TableHead>
250 <TableRow>
251 <TableCell>Link/Description</TableCell>
252 <TableCell>Type</TableCell>
253 <TableCell>User</TableCell>
254 <TableCell>Status</TableCell>
255 <TableCell align="right">Actions</TableCell>
256 </TableRow>
257 </TableHead>
258 <TableBody>
259 {data.componentSuggestions?.map((sug: any) => (
260 <TableRow key={sug.id}>
261 <TableCell sx={{minWidth: 200, maxWidth: 300}}>
262 <a
263 href={sug.link}
264 target="_blank"
265 rel="noopener noreferrer"
266 title={sug.link}
267 style={{textDecoration: 'none', color: 'inherit'}}
268 >
269 {sug.description || sug.link}
270 </a>
271 </TableCell>
272 <TableCell sx={{minWidth: 80}}>
273 {sug.componentType?.toUpperCase()}
274 </TableCell>
275 <TableCell sx={{minWidth: 100}}>
276 {sug.user?.username || `${sug.userId}`}
277 </TableCell>
278 <TableCell sx={{minWidth: 100}}>
279 <Chip
280 label={sug.status || 'pending'}
281 size="small"
282 color={
283 sug.status === 'approved' ? 'success' :
284 sug.status === 'rejected' ? 'error' :
285 'default'
286 }
287 />
288 </TableCell>
289 <TableCell align="right" sx={{minWidth: 150}}>
290 {sug.status === 'pending' ? (
291 <>
292 <IconButton
293 size="small"
294 color="success"
295 onClick={() => openSuggestionReview(sug.id, 'approved')}
296 >
297 <CheckCircleIcon/>
298 </IconButton>
299 <IconButton
300 size="small"
301 color="error"
302 onClick={() => openSuggestionReview(sug.id, 'rejected')}
303 >
304 <CancelIcon/>
305 </IconButton>
306 </>
307 ) : sug.status === 'approved' ? (
308 <Button
309 size="small"
310 variant="contained"
311 color="warning"
312 startIcon={<AddIcon/>}
313 onClick={() => setCreateComponentDialog({
314 open: true,
315 suggestion: sug
316 })}
317 >
318 Create
319 </Button>
320 ) : (
321 <Typography variant="caption" color="text.secondary">
322 {sug.adminComment || 'Rejected'}
323 </Typography>
324 )}
325 </TableCell>
326 </TableRow>
327 ))}
328 </TableBody>
329 </Table>
330 </Box>
331 )}
332 </Paper>
333
334 <Paper sx={{p: 3, borderLeft: '6px solid #ed6c02', bgcolor: '#121212'}}>
335 <Box sx={{display: 'flex', alignItems: 'center', mb: 2}}>
336 <BuildIcon color="warning" sx={{mr: 1}}/>
337 <Typography variant="h6" fontWeight="bold">
338 Builds Waiting for Approval ({data.pendingBuilds?.length || 0})
339 </Typography>
340 </Box>
341
342 {data.pendingBuilds?.length === 0 ? (
343 <Typography color="text.secondary">No pending builds.</Typography>
344 ) : (
345 <Box sx={{
346 display: 'grid',
347 gridTemplateColumns: {
348 xs: '1fr',
349 sm: 'repeat(2, 1fr)',
350 lg: 'repeat(3, 1fr)',
351 xl: 'repeat(4, 1fr)'
352 },
353 gap: 2
354 }}>
355 {data.pendingBuilds.map((build: any) => (
356 <Box key={build.id}>
357 <BuildCard
358 build={build}
359 onClick={() => setSelectedBuildId(build.id)}
360 />
361 <Box sx={{mt: 1, display: 'flex', gap: 1, justifyContent: 'center'}}>
362 <Button
363 variant="contained"
364 color="warning"
365 size="small"
366 startIcon={<BuildIcon/>}
367 onClick={(e) => {
368 e.stopPropagation();
369 openBuildApproval(build.id, build.name);
370 }}
371 >
372 Review
373 </Button>
374 </Box>
375 </Box>
376 ))}
377 </Box>
378 )}
379 </Paper>
380
381 <Paper sx={{p: 3, borderLeft: '6px solid #2e7d32'}}>
382 <Typography variant="h6" fontWeight="bold" gutterBottom>
383 My Builds / Sandbox
384 </Typography>
385 {data.userBuilds?.length === 0 ? (
386 <Typography color="text.secondary">No builds created by admin.</Typography>
387 ) : (
388 <Box sx={{
389 display: 'grid',
390 gridTemplateColumns: {
391 xs: '1fr',
392 sm: 'repeat(2, 1fr)',
393 lg: 'repeat(3, 1fr)',
394 xl: 'repeat(4, 1fr)'
395 },
396 gap: 2
397 }}>
398 {data.userBuilds.slice(0, 4).map((build: any) => (
399 <Box key={build.id}>
400 <BuildCard
401 build={build}
402 onClick={() => setSelectedBuildId(build.id)}
403 />
404 <Box sx={{mt: 1, display: 'flex', justifyContent: 'center'}}>
405 <Button
406 variant="outlined"
407 color="error"
408 size="small"
409 startIcon={<DeleteIcon/>}
410 onClick={(e) => {
411 e.stopPropagation();
412 openDeleteDialog(build.id, build.name);
413 }}
414 fullWidth
415 >
416 Delete
417 </Button>
418 </Box>
419 </Box>
420 ))}
421 </Box>
422 )}
423 </Paper>
424 </Box>
425 </Box>
426
427 <Dialog open={suggestionDialog.open}
428 onClose={() => setSuggestionDialog({...suggestionDialog, open: false})}>
429 <DialogTitle>Review Component Suggestion</DialogTitle>
430 <DialogContent>
431 <Typography gutterBottom>Status: <b>{suggestionDialog.action.toUpperCase()}</b></Typography>
432 <TextField
433 fullWidth
434 label="Admin Comment"
435 multiline
436 rows={3}
437 value={adminComment}
438 onChange={(e) => setAdminComment(e.target.value)}
439 sx={{mt: 2}}
440 />
441 </DialogContent>
442 <DialogActions>
443 <Button onClick={() => setSuggestionDialog({...suggestionDialog, open: false})}>Cancel</Button>
444 <Button variant="contained" onClick={submitSuggestionReview}>Submit</Button>
445 </DialogActions>
446 </Dialog>
447
448 <Dialog
449 open={buildApprovalDialog.open}
450 onClose={() => setBuildApprovalDialog({open: false, buildId: null, buildName: ''})}
451 >
452 <DialogTitle>Review Build</DialogTitle>
453 <DialogContent>
454 <Typography variant="h6" gutterBottom>{buildApprovalDialog.buildName}</Typography>
455 <Typography color="text.secondary" sx={{mb: 2}}>
456 Build ID: {buildApprovalDialog.buildId}
457 </Typography>
458 <TextField
459 fullWidth
460 multiline
461 rows={2}
462 label="Reject reason (optional)"
463 value={rejectReason}
464 onChange={(e) => setRejectReason(e.target.value)}
465 placeholder="Why reject this build?"
466 sx={{mt: 1}}
467 />
468 </DialogContent>
469 <DialogActions>
470 <Button
471 onClick={() => setBuildApprovalDialog({open: false, buildId: null, buildName: ''})}
472 disabled={approvalLoading}
473 >
474 Cancel
475 </Button>
476 <Button
477 onClick={handleRejectBuild}
478 variant="outlined"
479 color="error"
480 disabled={approvalLoading || !rejectReason.trim()}
481 >
482 {approvalLoading ? <CircularProgress size={20}/> : 'Reject'}
483 </Button>
484 <Button
485 onClick={handleApproveBuild}
486 variant="contained"
487 color="success"
488 disabled={approvalLoading}
489 >
490 {approvalLoading ? <CircularProgress size={20}/> : 'Approve'}
491 </Button>
492 </DialogActions>
493 </Dialog>
494
495 <Dialog
496 open={deleteDialog.open}
497 onClose={() => setDeleteDialog({open: false, buildId: null, buildName: ''})}
498 >
499 <DialogTitle>Delete Build</DialogTitle>
500 <DialogContent>
501 <Typography variant="h6" gutterBottom>{deleteDialog.buildName}</Typography>
502 <Typography color="text.secondary">
503 Are you sure you want to permanently delete this build? This action cannot be undone.
504 </Typography>
505 </DialogContent>
506 <DialogActions>
507 <Button
508 onClick={() => setDeleteDialog({open: false, buildId: null, buildName: ''})}
509 disabled={deleteLoading}
510 >
511 Cancel
512 </Button>
513 <Button
514 onClick={handleDelete}
515 variant="contained"
516 color="error"
517 disabled={deleteLoading}
518 startIcon={deleteLoading ? <CircularProgress size={20}/> : <DeleteIcon/>}
519 >
520 {deleteLoading ? 'Deleting...' : 'Delete Build'}
521 </Button>
522 </DialogActions>
523 </Dialog>
524
525 <BuildDetailsDialog
526 open={!!selectedBuildId}
527 buildId={selectedBuildId!}
528 currentUser={data.admin}
529 onClose={() => setSelectedBuildId(null)}
530 isDashboardView={true}
531 />
532
533 <AddComponentDialog
534 open={addDialogOpen}
535 onClose={() => setAddDialogOpen(false)}
536 onSuccess={() => {
537 loadData();
538 setAddDialogOpen(false);
539 }}
540 />
541
542 <AddComponentDialog
543 open={createComponentDialog.open}
544 onClose={() => setCreateComponentDialog({open: false, suggestion: null})}
545 onSuccess={() => {
546 loadData();
547 setCreateComponentDialog({open: false, suggestion: null});
548 }}
549 prefillData={createComponentDialog.suggestion ? {
550 type: createComponentDialog.suggestion.componentType,
551 suggestionLink: createComponentDialog.suggestion.link,
552 suggestionDescription: createComponentDialog.suggestion.description
553 } : undefined}
554 />
555 </Container>
556 );
557}
Note: See TracBrowser for help on using the repository browser.