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

main
Last change on this file since be22289 was 87b79bc, checked in by Mihail <mihail2.naumov@…>, 6 months ago

Fixed suggests component and also added hamburger menu for small screens for the navbar.

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