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