Update customer management functionality by adding modification support, enhancing the customer model to include modifications, and improving the UI for customer and modification forms. Refactor API routes for better error handling and streamline customer data management.

This commit is contained in:
Alvin
2025-06-10 11:38:27 +02:00
parent ec07b89dab
commit 1ac852f2a1
5 changed files with 329 additions and 145 deletions

14
client/public/favicon.svg Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Car body -->
<path d="M6 20C6 18.8954 6.89543 18 8 18H24C25.1046 18 26 18.8954 26 20V24C26 25.1046 25.1046 26 24 26H8C6.89543 26 6 25.1046 6 24V20Z" fill="#ff3d00"/>
<!-- Car top -->
<path d="M10 14C10 12.8954 10.8954 12 12 12H20C21.1046 12 22 12.8954 22 14V18H10V14Z" fill="#ff3d00"/>
<!-- Wheels -->
<circle cx="10" cy="22" r="3" fill="#212121"/>
<circle cx="22" cy="22" r="3" fill="#212121"/>
<!-- Speed lines -->
<path d="M4 16L8 16" stroke="#ff3d00" stroke-width="2" stroke-linecap="round"/>
<path d="M4 18L8 18" stroke="#ff3d00" stroke-width="2" stroke-linecap="round"/>
<path d="M4 20L8 20" stroke="#ff3d00" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Car Tuning & Modification Management System"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.svg" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>AutoTune Pro | Car Tuning Management</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -22,40 +22,47 @@ import {
Select, Select,
FormControl, FormControl,
InputLabel, InputLabel,
Grid,
Card,
CardContent,
Divider
} from '@mui/material'; } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit'; import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon } from '@mui/icons-material';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import axios from 'axios'; import axios from 'axios';
const carModels = [ const carModels = [
{ value: 'bmw_m3', label: 'BMW M3' }, 'BMW M3',
{ value: 'bmw_m4', label: 'BMW M4' }, 'BMW M4',
{ value: 'audi_rs3', label: 'Audi RS3' }, 'Audi RS3',
{ value: 'audi_rs4', label: 'Audi RS4' }, 'Audi RS4',
{ value: 'mercedes_c63', label: 'Mercedes-AMG C63' }, 'Mercedes-AMG C63',
{ value: 'mercedes_e63', label: 'Mercedes-AMG E63' }, 'Mercedes-AMG E63',
{ value: 'volkswagen_golf_r', label: 'Volkswagen Golf R' }, 'Volkswagen Golf R',
{ value: 'volkswagen_arteon_r', label: 'Volkswagen Arteon R' }, 'Volkswagen Arteon R',
{ value: 'toyota_supra', label: 'Toyota Supra' }, 'Toyota Supra',
{ value: 'nissan_gtr', label: 'Nissan GT-R' }, 'Nissan GT-R',
{ value: 'porsche_911', label: 'Porsche 911' }, 'Porsche 911',
{ value: 'porsche_cayman', label: 'Porsche Cayman' }, 'Porsche Cayman',
{ value: 'honda_civic_type_r', label: 'Honda Civic Type R' }, 'Honda Civic Type R',
{ value: 'subaru_wrx_sti', label: 'Subaru WRX STI' }, 'Subaru WRX STI',
{ value: 'mitsubishi_evo', label: 'Mitsubishi Lancer Evolution' }, 'Mitsubishi Lancer Evolution',
{ value: 'other', label: 'Other' }, 'Other'
];
const categories = [
'Engine',
'Exhaust',
'Suspension',
'Brakes',
'Wheels'
]; ];
// Create axios instance with default config // Create axios instance with default config
const api = axios.create({ const api = axios.create({
baseURL: 'http://localhost:5000/api', baseURL: 'http://localhost:5000/api'
headers: {
'Content-Type': 'application/json',
},
}); });
// Add request interceptor to add token to all requests // Add request interceptor to include token
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
@@ -72,17 +79,24 @@ api.interceptors.request.use(
const CustomerManagement = () => { const CustomerManagement = () => {
const [customers, setCustomers] = useState([]); const [customers, setCustomers] = useState([]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [openModDialog, setOpenModDialog] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null); const [selectedCustomer, setSelectedCustomer] = useState(null);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
email: '', email: '',
phone: '', phone: '',
address: '', address: '',
carModel: '', carModel: '',
carYear: '', carYear: ''
});
const [modificationData, setModificationData] = useState({
name: '',
description: '',
price: '',
category: ''
}); });
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
useEffect(() => { useEffect(() => {
fetchCustomers(); fetchCustomers();
@@ -99,32 +113,56 @@ const CustomerManagement = () => {
const handleOpen = (customer = null) => { const handleOpen = (customer = null) => {
if (customer) { if (customer) {
setSelectedCustomer(customer);
setFormData(customer); setFormData(customer);
setSelectedCustomer(customer);
} else { } else {
setSelectedCustomer(null);
setFormData({ setFormData({
name: '', name: '',
email: '', email: '',
phone: '', phone: '',
address: '', address: '',
carModel: '', carModel: '',
carYear: '', carYear: ''
}); });
setSelectedCustomer(null);
} }
setOpen(true); setOpen(true);
}; };
const handleOpenModDialog = (customer) => {
setSelectedCustomer(customer);
setModificationData({
name: '',
description: '',
price: '',
category: ''
});
setOpenModDialog(true);
};
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
setError(''); setError('');
setSuccess(''); setSuccess('');
}; };
const handleChange = (e) => { const handleCloseModDialog = () => {
setOpenModDialog(false);
setError('');
setSuccess('');
};
const handleInputChange = (e) => {
setFormData({ setFormData({
...formData, ...formData,
[e.target.name]: e.target.value, [e.target.name]: e.target.value
});
};
const handleModInputChange = (e) => {
setModificationData({
...modificationData,
[e.target.name]: e.target.value
}); });
}; };
@@ -145,6 +183,18 @@ const CustomerManagement = () => {
} }
}; };
const handleAddModification = async (e) => {
e.preventDefault();
try {
await api.put(`/customers/${selectedCustomer._id}/modifications`, modificationData);
setSuccess('Modification added successfully');
fetchCustomers();
handleCloseModDialog();
} catch (err) {
setError(err.response?.data?.message || 'An error occurred');
}
};
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (window.confirm('Are you sure you want to delete this customer?')) { if (window.confirm('Are you sure you want to delete this customer?')) {
try { try {
@@ -159,8 +209,8 @@ const CustomerManagement = () => {
return ( return (
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}> <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4" component="h1" gutterBottom sx={{ fontWeight: 'bold' }}> <Typography variant="h4" component="h1" gutterBottom>
Customer Management Customer Management
</Typography> </Typography>
<Button <Button
@@ -173,17 +223,8 @@ const CustomerManagement = () => {
</Button> </Button>
</Box> </Box>
{error && ( {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Alert severity="error" sx={{ mb: 2 }}> {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 2 }}>
{success}
</Alert>
)}
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table> <Table>
@@ -194,6 +235,7 @@ const CustomerManagement = () => {
<TableCell>Phone</TableCell> <TableCell>Phone</TableCell>
<TableCell>Car Model</TableCell> <TableCell>Car Model</TableCell>
<TableCell>Car Year</TableCell> <TableCell>Car Year</TableCell>
<TableCell>Modifications</TableCell>
<TableCell>Actions</TableCell> <TableCell>Actions</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@@ -205,6 +247,24 @@ const CustomerManagement = () => {
<TableCell>{customer.phone}</TableCell> <TableCell>{customer.phone}</TableCell>
<TableCell>{customer.carModel}</TableCell> <TableCell>{customer.carModel}</TableCell>
<TableCell>{customer.carYear}</TableCell> <TableCell>{customer.carYear}</TableCell>
<TableCell>
<Button
variant="outlined"
size="small"
onClick={() => handleOpenModDialog(customer)}
>
Add Modification
</Button>
{customer.modifications && customer.modifications.length > 0 && (
<Box sx={{ mt: 1 }}>
{customer.modifications.map((mod, index) => (
<Typography key={index} variant="body2">
{mod.name} - {mod.category}
</Typography>
))}
</Box>
)}
</TableCell>
<TableCell> <TableCell>
<IconButton onClick={() => handleOpen(customer)} color="primary"> <IconButton onClick={() => handleOpen(customer)} color="primary">
<EditIcon /> <EditIcon />
@@ -219,82 +279,157 @@ const CustomerManagement = () => {
</Table> </Table>
</TableContainer> </TableContainer>
{/* Customer Form Dialog */}
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle> <DialogTitle>{selectedCustomer ? 'Edit Customer' : 'Add Customer'}</DialogTitle>
{selectedCustomer ? 'Edit Customer' : 'Add New Customer'} <DialogContent>
</DialogTitle> <Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
<form onSubmit={handleSubmit}> <Grid container spacing={2}>
<DialogContent> <Grid item xs={12}>
<TextField <TextField
fullWidth fullWidth
label="Name" label="Full Name"
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleInputChange}
margin="normal" required
required />
/> </Grid>
<TextField <Grid item xs={12}>
fullWidth <TextField
label="Email" fullWidth
name="email" label="Email Address"
type="email" name="email"
value={formData.email} type="email"
onChange={handleChange} value={formData.email}
margin="normal" onChange={handleInputChange}
required required
/> />
<TextField </Grid>
fullWidth <Grid item xs={12}>
label="Phone" <TextField
name="phone" fullWidth
value={formData.phone} label="Phone Number"
onChange={handleChange} name="phone"
margin="normal" value={formData.phone}
required onChange={handleInputChange}
/> required
<TextField />
fullWidth </Grid>
label="Address" <Grid item xs={12}>
name="address" <TextField
value={formData.address} fullWidth
onChange={handleChange} label="Address"
margin="normal" name="address"
multiline value={formData.address}
rows={2} onChange={handleInputChange}
/> required
<FormControl fullWidth margin="normal" required> />
<InputLabel>Car Model</InputLabel> </Grid>
<Select <Grid item xs={12}>
name="carModel" <FormControl fullWidth required>
value={formData.carModel} <InputLabel>Car Model</InputLabel>
onChange={handleChange} <Select
label="Car Model" name="carModel"
> value={formData.carModel}
{carModels.map((model) => ( onChange={handleInputChange}
<MenuItem key={model.value} value={model.value}> label="Car Model"
{model.label} >
</MenuItem> {carModels.map((model) => (
))} <MenuItem key={model} value={model}>
</Select> {model}
</FormControl> </MenuItem>
<TextField ))}
fullWidth </Select>
label="Car Year" </FormControl>
name="carYear" </Grid>
value={formData.carYear} <Grid item xs={12}>
onChange={handleChange} <TextField
margin="normal" fullWidth
required label="Car Year"
/> name="carYear"
</DialogContent> type="number"
<DialogActions> value={formData.carYear}
<Button onClick={handleClose}>Cancel</Button> onChange={handleInputChange}
<Button type="submit" variant="contained" color="primary"> required
{selectedCustomer ? 'Update' : 'Add'} />
</Button> </Grid>
</DialogActions> </Grid>
</form> </Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSubmit} variant="contained" color="primary">
{selectedCustomer ? 'Update' : 'Add'}
</Button>
</DialogActions>
</Dialog>
{/* Modification Form Dialog */}
<Dialog open={openModDialog} onClose={handleCloseModDialog} maxWidth="sm" fullWidth>
<DialogTitle>Add Modification</DialogTitle>
<DialogContent>
<Box component="form" onSubmit={handleAddModification} sx={{ mt: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
label="Modification Name"
name="name"
value={modificationData.name}
onChange={handleModInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Description"
name="description"
multiline
rows={3}
value={modificationData.description}
onChange={handleModInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Price"
name="price"
type="number"
value={modificationData.price}
onChange={handleModInputChange}
required
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth required>
<InputLabel>Category</InputLabel>
<Select
name="category"
value={modificationData.category}
onChange={handleModInputChange}
label="Category"
>
{categories.map((category) => (
<MenuItem key={category} value={category}>
{category}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseModDialog}>Cancel</Button>
<Button onClick={handleAddModification} variant="contained" color="primary">
Add Modification
</Button>
</DialogActions>
</Dialog> </Dialog>
</Container> </Container>
); );

View File

@@ -20,6 +20,7 @@ const customerSchema = new mongoose.Schema({
}, },
address: { address: {
type: String, type: String,
required: true,
trim: true trim: true
}, },
carModel: { carModel: {
@@ -28,10 +29,31 @@ const customerSchema = new mongoose.Schema({
trim: true trim: true
}, },
carYear: { carYear: {
type: String, type: Number,
required: true, required: true
trim: true
}, },
modifications: [{
name: {
type: String,
required: true
},
description: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
category: {
type: String,
required: true
},
dateAdded: {
type: Date,
default: Date.now
}
}],
createdAt: { createdAt: {
type: Date, type: Date,
default: Date.now default: Date.now
@@ -48,4 +70,6 @@ customerSchema.pre('save', function(next) {
next(); next();
}); });
module.exports = mongoose.model('Customer', customerSchema); const Customer = mongoose.model('Customer', customerSchema);
module.exports = Customer;

View File

@@ -9,7 +9,7 @@ router.get('/', auth, async (req, res) => {
const customers = await Customer.find().sort({ name: 1 }); const customers = await Customer.find().sort({ name: 1 });
res.json(customers); res.json(customers);
} catch (err) { } catch (err) {
res.status(500).json({ message: err.message }); res.status(500).send('Server Error');
} }
}); });
@@ -22,24 +22,16 @@ router.get('/:id', auth, async (req, res) => {
} }
res.json(customer); res.json(customer);
} catch (err) { } catch (err) {
res.status(500).json({ message: err.message }); res.status(500).send('Server Error');
} }
}); });
// Create customer // Create customer
router.post('/', auth, async (req, res) => { router.post('/', auth, async (req, res) => {
const customer = new Customer({
name: req.body.name,
email: req.body.email,
phone: req.body.phone,
address: req.body.address,
carModel: req.body.carModel,
carYear: req.body.carYear,
});
try { try {
const newCustomer = await customer.save(); const customer = new Customer(req.body);
res.status(201).json(newCustomer); await customer.save();
res.status(201).json(customer);
} catch (err) { } catch (err) {
res.status(400).json({ message: err.message }); res.status(400).json({ message: err.message });
} }
@@ -53,9 +45,28 @@ router.put('/:id', auth, async (req, res) => {
return res.status(404).json({ message: 'Customer not found' }); return res.status(404).json({ message: 'Customer not found' });
} }
Object.assign(customer, req.body); Object.keys(req.body).forEach(key => {
const updatedCustomer = await customer.save(); customer[key] = req.body[key];
res.json(updatedCustomer); });
await customer.save();
res.json(customer);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// Add modification to customer
router.put('/:id/modifications', auth, async (req, res) => {
try {
const customer = await Customer.findById(req.params.id);
if (!customer) {
return res.status(404).json({ message: 'Customer not found' });
}
customer.modifications.push(req.body);
await customer.save();
res.json(customer);
} catch (err) { } catch (err) {
res.status(400).json({ message: err.message }); res.status(400).json({ message: err.message });
} }
@@ -72,7 +83,7 @@ router.delete('/:id', auth, async (req, res) => {
await customer.remove(); await customer.remove();
res.json({ message: 'Customer deleted' }); res.json({ message: 'Customer deleted' });
} catch (err) { } catch (err) {
res.status(500).json({ message: err.message }); res.status(500).send('Server Error');
} }
}); });