source: pages/dashboard/admin/+Page.tsx@ 958bc89

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

Added add component button, fixed several things

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