| [958bc89] | 1 | import React, { useState, useEffect } from 'react';
|
|---|
| 2 | import {
|
|---|
| 3 | Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField,
|
|---|
| 4 | Grid, Box, Typography, MenuItem, IconButton, Avatar, Alert
|
|---|
| 5 | } from '@mui/material';
|
|---|
| 6 | import AddIcon from '@mui/icons-material/Add';
|
|---|
| 7 | import DeleteIcon from '@mui/icons-material/Delete';
|
|---|
| 8 | import CloseIcon from '@mui/icons-material/Close';
|
|---|
| 9 | import { onCreateNewComponent, onGetDetailsForNewComponent } from '../pages/dashboard/admin/adminDashboard.telefunc';
|
|---|
| 10 |
|
|---|
| 11 | const COMPONENT_TYPES = [
|
|---|
| 12 | { value: 'cpu', label: 'CPU' },
|
|---|
| 13 | { value: 'gpu', label: 'Graphics Card' },
|
|---|
| 14 | { value: 'motherboard', label: 'Motherboard' },
|
|---|
| 15 | { value: 'memory', label: 'Memory' },
|
|---|
| 16 | { value: 'storage', label: 'Storage' },
|
|---|
| 17 | { value: 'power_supply', label: 'Power Supply' },
|
|---|
| 18 | { value: 'case', label: 'Case' },
|
|---|
| 19 | { value: 'cooler', label: 'CPU Cooler' },
|
|---|
| 20 | ];
|
|---|
| 21 |
|
|---|
| 22 | export default function AddComponentDialog({ open, onClose, onSuccess }: {
|
|---|
| 23 | open: boolean;
|
|---|
| 24 | onClose: () => void;
|
|---|
| 25 | onSuccess: () => void;
|
|---|
| 26 | }) {
|
|---|
| 27 | const [name, setName] = useState('');
|
|---|
| 28 | const [brand, setBrand] = useState('');
|
|---|
| 29 | const [price, setPrice] = useState('');
|
|---|
| 30 | const [imgUrl, setImgUrl] = useState('');
|
|---|
| 31 | const [type, setType] = useState('');
|
|---|
| 32 |
|
|---|
| 33 | const [requiredFields, setRequiredFields] = useState<any[]>([]);
|
|---|
| 34 | const [multiTables, setMultiTables] = useState<any>({});
|
|---|
| 35 | const [specificData, setSpecificData] = useState<any>({});
|
|---|
| 36 |
|
|---|
| 37 | const [loading, setLoading] = useState(false);
|
|---|
| 38 | const [error, setError] = useState('');
|
|---|
| 39 |
|
|---|
| 40 | useEffect(() => {
|
|---|
| 41 | if (type) {
|
|---|
| 42 | setLoading(true);
|
|---|
| 43 | onGetDetailsForNewComponent({ type })
|
|---|
| 44 | .then((details) => {
|
|---|
| 45 | console.log('Component details:', details);
|
|---|
| 46 |
|
|---|
| 47 | setRequiredFields(details.requiredFields || []);
|
|---|
| 48 | setMultiTables(details.multiTables || {});
|
|---|
| 49 |
|
|---|
| 50 | const initialData: any = {};
|
|---|
| 51 |
|
|---|
| 52 | (details.requiredFields || []).forEach((field: any) => {
|
|---|
| 53 | const fieldName = typeof field === 'string' ? field : field.name;
|
|---|
| 54 | initialData[fieldName] = '';
|
|---|
| 55 | });
|
|---|
| 56 |
|
|---|
| 57 | Object.keys(details.multiTables || {}).forEach((tableName) => {
|
|---|
| 58 | initialData[tableName] = [];
|
|---|
| 59 | });
|
|---|
| 60 |
|
|---|
| 61 | setSpecificData(initialData);
|
|---|
| 62 | })
|
|---|
| 63 | .catch((err) => {
|
|---|
| 64 | console.error('Field load error:', err);
|
|---|
| 65 | setError('Failed to load component fields');
|
|---|
| 66 | })
|
|---|
| 67 | .finally(() => setLoading(false));
|
|---|
| 68 | }
|
|---|
| 69 | }, [type]);
|
|---|
| 70 |
|
|---|
| 71 | const handleFieldChange = (fieldName: string, value: any) => {
|
|---|
| 72 | setSpecificData((prev: any) => ({
|
|---|
| 73 | ...prev,
|
|---|
| 74 | [fieldName]: value
|
|---|
| 75 | }));
|
|---|
| 76 | };
|
|---|
| 77 |
|
|---|
| 78 | const handleAddMultiTableRow = (tableName: string) => {
|
|---|
| 79 | const tableConfig = multiTables[tableName];
|
|---|
| 80 | const newRow: any = {};
|
|---|
| 81 |
|
|---|
| 82 | tableConfig.forEach((field: string) => {
|
|---|
| 83 | newRow[field] = '';
|
|---|
| 84 | });
|
|---|
| 85 |
|
|---|
| 86 | setSpecificData((prev: any) => ({
|
|---|
| 87 | ...prev,
|
|---|
| 88 | [tableName]: [...(prev[tableName] || []), newRow]
|
|---|
| 89 | }));
|
|---|
| 90 | };
|
|---|
| 91 |
|
|---|
| 92 | const handleRemoveMultiTableRow = (tableName: string, index: number) => {
|
|---|
| 93 | setSpecificData((prev: any) => ({
|
|---|
| 94 | ...prev,
|
|---|
| 95 | [tableName]: prev[tableName].filter((_: any, i: number) => i !== index)
|
|---|
| 96 | }));
|
|---|
| 97 | };
|
|---|
| 98 |
|
|---|
| 99 | const handleMultiTableFieldChange = (tableName: string, index: number, fieldName: string, value: any) => {
|
|---|
| 100 | setSpecificData((prev: any) => {
|
|---|
| 101 | const updated = [...prev[tableName]];
|
|---|
| 102 | updated[index] = { ...updated[index], [fieldName]: value };
|
|---|
| 103 | return { ...prev, [tableName]: updated };
|
|---|
| 104 | });
|
|---|
| 105 | };
|
|---|
| 106 |
|
|---|
| 107 | const handleSubmit = async () => {
|
|---|
| 108 | setError('');
|
|---|
| 109 |
|
|---|
| 110 | if (!name.trim()) return setError('Name is required');
|
|---|
| 111 | if (!brand.trim()) return setError('Brand is required');
|
|---|
| 112 | if (!price || isNaN(Number(price)) || Number(price) <= 0) return setError('Valid price is required');
|
|---|
| 113 | if (!imgUrl.trim()) return setError('Image URL is required');
|
|---|
| 114 | if (!type) return setError('Component type is required');
|
|---|
| 115 |
|
|---|
| 116 | setLoading(true);
|
|---|
| 117 | try {
|
|---|
| 118 | await onCreateNewComponent({
|
|---|
| 119 | name: name.trim(),
|
|---|
| 120 | brand: brand.trim(),
|
|---|
| 121 | price: Number(price),
|
|---|
| 122 | imgUrl: imgUrl.trim(),
|
|---|
| 123 | type,
|
|---|
| 124 | specificData
|
|---|
| 125 | });
|
|---|
| 126 |
|
|---|
| 127 | onSuccess();
|
|---|
| 128 | handleClose();
|
|---|
| 129 | } catch (err) {
|
|---|
| 130 | setError('Failed to create component. Please check all fields.');
|
|---|
| 131 | } finally {
|
|---|
| 132 | setLoading(false);
|
|---|
| 133 | }
|
|---|
| 134 | };
|
|---|
| 135 |
|
|---|
| 136 | const handleClose = () => {
|
|---|
| 137 | setName('');
|
|---|
| 138 | setBrand('');
|
|---|
| 139 | setPrice('');
|
|---|
| 140 | setImgUrl('');
|
|---|
| 141 | setType('');
|
|---|
| 142 | setSpecificData({});
|
|---|
| 143 | setRequiredFields([]);
|
|---|
| 144 | setMultiTables({});
|
|---|
| 145 | setError('');
|
|---|
| 146 | onClose();
|
|---|
| 147 | };
|
|---|
| 148 |
|
|---|
| 149 | const formatFieldLabel = (fieldName: string): string => {
|
|---|
| 150 | return fieldName
|
|---|
| 151 | .replace(/([A-Z])/g, ' $1')
|
|---|
| 152 | .replace(/^./, (str: string) => str.toUpperCase());
|
|---|
| 153 | };
|
|---|
| 154 |
|
|---|
| 155 | const renderAllFields = () => {
|
|---|
| 156 | const allFields = [...(requiredFields || []), ...(Object.values(multiTables).flat() || [])];
|
|---|
| 157 |
|
|---|
| 158 | return (
|
|---|
| 159 | <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|---|
| 160 | {allFields.map((field: any, index: number) => {
|
|---|
| 161 | const fieldName = typeof field === 'string' ? field : field.name;
|
|---|
| 162 |
|
|---|
| 163 | const isNumber = fieldName.includes('height') || fieldName.includes('length') ||
|
|---|
| 164 | fieldName.includes('wattage') || fieldName.includes('capacity') ||
|
|---|
| 165 | fieldName.includes('speed') || fieldName.includes('cores') ||
|
|---|
| 166 | fieldName.includes('threads') || fieldName.includes('vram') ||
|
|---|
| 167 | fieldName.includes('slots') || fieldName.includes('numSlots');
|
|---|
| 168 |
|
|---|
| 169 | return (
|
|---|
| 170 | <TextField
|
|---|
| 171 | key={`${fieldName}-${index}`}
|
|---|
| 172 | fullWidth
|
|---|
| 173 | label={formatFieldLabel(fieldName)}
|
|---|
| 174 | value={specificData[fieldName] || ''}
|
|---|
| 175 | onChange={(e) => handleFieldChange(fieldName, isNumber ? Number(e.target.value) : e.target.value)}
|
|---|
| 176 | type={isNumber ? 'number' : 'text'}
|
|---|
| 177 | required
|
|---|
| 178 | sx={{ mb: 2 }}
|
|---|
| 179 | />
|
|---|
| 180 | );
|
|---|
| 181 | })}
|
|---|
| 182 | </Box>
|
|---|
| 183 | );
|
|---|
| 184 | };
|
|---|
| 185 |
|
|---|
| 186 | return (
|
|---|
| 187 | <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth scroll="paper">
|
|---|
| 188 | <DialogTitle sx={{ bgcolor: '#ff8201', color: 'white', display: 'flex', justifyContent: 'space-between', alignItems: 'column' }}>
|
|---|
| 189 | <Typography variant="h6" fontWeight="bold">Add New Component</Typography>
|
|---|
| 190 | <IconButton onClick={handleClose} sx={{ color: 'white' }}>
|
|---|
| 191 | <CloseIcon />
|
|---|
| 192 | </IconButton>
|
|---|
| 193 | </DialogTitle>
|
|---|
| 194 |
|
|---|
| 195 | <DialogContent sx={{ p: 3 }}>
|
|---|
| 196 | {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
|---|
| 197 |
|
|---|
| 198 | <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|---|
| 199 | <Box>
|
|---|
| 200 | <Typography variant="subtitle1" fontWeight="bold" gutterBottom sx={{ mb: 2 }}>
|
|---|
| 201 | Basic Information
|
|---|
| 202 | </Typography>
|
|---|
| 203 |
|
|---|
| 204 | <TextField
|
|---|
| 205 | fullWidth
|
|---|
| 206 | select
|
|---|
| 207 | label="Component Type"
|
|---|
| 208 | value={type}
|
|---|
| 209 | onChange={(e) => setType(e.target.value)}
|
|---|
| 210 | required
|
|---|
| 211 | sx={{ mb: 2 }}
|
|---|
| 212 | >
|
|---|
| 213 | {COMPONENT_TYPES.map((option) => (
|
|---|
| 214 | <MenuItem key={option.value} value={option.value}>
|
|---|
| 215 | {option.label}
|
|---|
| 216 | </MenuItem>
|
|---|
| 217 | ))}
|
|---|
| 218 | </TextField>
|
|---|
| 219 |
|
|---|
| 220 | <Grid container spacing={2} sx={{ mb: 2 }}>
|
|---|
| 221 | <Grid item xs={12} md={6}>
|
|---|
| 222 | <TextField
|
|---|
| 223 | fullWidth
|
|---|
| 224 | label="Brand"
|
|---|
| 225 | value={brand}
|
|---|
| 226 | onChange={(e) => setBrand(e.target.value)}
|
|---|
| 227 | required
|
|---|
| 228 | />
|
|---|
| 229 | </Grid>
|
|---|
| 230 | <Grid item xs={12} md={6}>
|
|---|
| 231 | <TextField
|
|---|
| 232 | fullWidth
|
|---|
| 233 | label="Name"
|
|---|
| 234 | value={name}
|
|---|
| 235 | onChange={(e) => setName(e.target.value)}
|
|---|
| 236 | required
|
|---|
| 237 | />
|
|---|
| 238 | </Grid>
|
|---|
| 239 | </Grid>
|
|---|
| 240 |
|
|---|
| 241 | <Grid container spacing={2}>
|
|---|
| 242 | <Grid item xs={12} md={6}>
|
|---|
| 243 | <TextField
|
|---|
| 244 | fullWidth
|
|---|
| 245 | label="Price (EUR)"
|
|---|
| 246 | type="number"
|
|---|
| 247 | value={price}
|
|---|
| 248 | onChange={(e) => setPrice(e.target.value)}
|
|---|
| 249 | required
|
|---|
| 250 | inputProps={{ step: '0.01', min: '0' }}
|
|---|
| 251 | />
|
|---|
| 252 | </Grid>
|
|---|
| 253 | <Grid item xs={12} md={6}>
|
|---|
| 254 | <TextField
|
|---|
| 255 | fullWidth
|
|---|
| 256 | label="Image URL"
|
|---|
| 257 | value={imgUrl}
|
|---|
| 258 | onChange={(e) => setImgUrl(e.target.value)}
|
|---|
| 259 | required
|
|---|
| 260 | />
|
|---|
| 261 | </Grid>
|
|---|
| 262 | </Grid>
|
|---|
| 263 | </Box>
|
|---|
| 264 |
|
|---|
| 265 | {imgUrl && (
|
|---|
| 266 | <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
|
|---|
| 267 | <Avatar
|
|---|
| 268 | src={imgUrl}
|
|---|
| 269 | variant="rounded"
|
|---|
| 270 | sx={{ width: 120, height: 120, bgcolor: '#f5f5f5' }}
|
|---|
| 271 | />
|
|---|
| 272 | </Box>
|
|---|
| 273 | )}
|
|---|
| 274 |
|
|---|
| 275 | {type && (requiredFields.length > 0 || Object.keys(multiTables).length > 0) && (
|
|---|
| 276 | <Box>
|
|---|
| 277 | <Typography variant="subtitle1" fontWeight="bold" gutterBottom sx={{ mb: 2 }}>
|
|---|
| 278 | {COMPONENT_TYPES.find(ct => ct.value === type)?.label} Specifications
|
|---|
| 279 | </Typography>
|
|---|
| 280 | {renderAllFields()}
|
|---|
| 281 | </Box>
|
|---|
| 282 | )}
|
|---|
| 283 |
|
|---|
| 284 | {Object.keys(multiTables).map((tableName) => (
|
|---|
| 285 | <Box key={tableName}>
|
|---|
| 286 | <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|---|
| 287 | <Typography variant="subtitle1" fontWeight="bold">
|
|---|
| 288 | {tableName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
|---|
| 289 | </Typography>
|
|---|
| 290 | <Button
|
|---|
| 291 | size="small"
|
|---|
| 292 | startIcon={<AddIcon />}
|
|---|
| 293 | onClick={() => handleAddMultiTableRow(tableName)}
|
|---|
| 294 | variant="outlined"
|
|---|
| 295 | >
|
|---|
| 296 | Add Row
|
|---|
| 297 | </Button>
|
|---|
| 298 | </Box>
|
|---|
| 299 |
|
|---|
| 300 | {(specificData[tableName] || []).map((row: any, index: number) => (
|
|---|
| 301 | <Box key={index} sx={{ p: 3, mb: 2, border: '1px solid #e0e0e0', borderRadius: 2, bgcolor: '#fafafa' }}>
|
|---|
| 302 | <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|---|
| 303 | <Typography variant="subtitle2" fontWeight="medium">
|
|---|
| 304 | Row {index + 1}
|
|---|
| 305 | </Typography>
|
|---|
| 306 | <IconButton
|
|---|
| 307 | size="small"
|
|---|
| 308 | color="error"
|
|---|
| 309 | onClick={() => handleRemoveMultiTableRow(tableName, index)}
|
|---|
| 310 | >
|
|---|
| 311 | <DeleteIcon />
|
|---|
| 312 | </IconButton>
|
|---|
| 313 | </Box>
|
|---|
| 314 |
|
|---|
| 315 | <Grid container spacing={2}>
|
|---|
| 316 | {multiTables[tableName].map((fieldName: string) => (
|
|---|
| 317 | <Grid item xs={12} sm={6} md={4} key={fieldName}>
|
|---|
| 318 | <TextField
|
|---|
| 319 | fullWidth
|
|---|
| 320 | size="small"
|
|---|
| 321 | label={fieldName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
|---|
| 322 | value={row[fieldName] || ''}
|
|---|
| 323 | onChange={(e) => handleMultiTableFieldChange(tableName, index, fieldName, e.target.value)}
|
|---|
| 324 | />
|
|---|
| 325 | </Grid>
|
|---|
| 326 | ))}
|
|---|
| 327 | </Grid>
|
|---|
| 328 | </Box>
|
|---|
| 329 | ))}
|
|---|
| 330 | </Box>
|
|---|
| 331 | ))}
|
|---|
| 332 | </Box>
|
|---|
| 333 | </DialogContent>
|
|---|
| 334 |
|
|---|
| 335 | <DialogActions sx={{ p: 3 }}>
|
|---|
| 336 | <Button onClick={handleClose} disabled={loading}>
|
|---|
| 337 | Cancel
|
|---|
| 338 | </Button>
|
|---|
| 339 | <Button
|
|---|
| 340 | variant="contained"
|
|---|
| 341 | color="primary"
|
|---|
| 342 | onClick={handleSubmit}
|
|---|
| 343 | disabled={loading || !type}
|
|---|
| 344 | >
|
|---|
| 345 | {loading ? 'Creating...' : 'Create Component'}
|
|---|
| 346 | </Button>
|
|---|
| 347 | </DialogActions>
|
|---|
| 348 | </Dialog>
|
|---|
| 349 | );
|
|---|
| 350 | }
|
|---|