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

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

Added delete button and still fixing naming problem with build creation

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