source: pages/dashboard/admin/+Page.tsx@ 4f2900a

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

Fixed build review

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