source: pages/dashboard/admin/+Page.tsx@ 5af32f0

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

Added styling to admin dashboard functions and highest ranking popup

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