source: pages/dashboard/admin/+Page.tsx@ 546a194

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

Optimized sites (changed from deprecated Grid to Box+CSS Grid)

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