Enhance App component by adding Register and BackToHome routes, and update CustomerList to CustomerManagement for improved customer management functionality. Refactor CustomerManagement to utilize a centralized axios instance for API calls and implement a dropdown for car model selection.

This commit is contained in:
Alvin
2025-06-10 10:24:25 +02:00
parent cefd020db6
commit ec07b89dab
5 changed files with 294 additions and 40 deletions

View File

@@ -3,13 +3,16 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-d
import { ThemeProvider, createTheme } from '@mui/material/styles'; import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline'; import CssBaseline from '@mui/material/CssBaseline';
import Login from './components/Login'; import Login from './components/Login';
import Register from './components/Register';
import Dashboard from './components/Dashboard'; import Dashboard from './components/Dashboard';
import CustomerList from './components/CustomerList'; import CustomerList from './components/CustomerList';
import CustomerDetail from './components/CustomerDetail'; import CustomerDetail from './components/CustomerDetail';
import CarModifications from './components/CarModifications'; import CarModifications from './components/CarModifications';
import ContactHistory from './components/ContactHistory'; import ContactHistory from './components/ContactHistory';
import CustomerManagement from './components/CustomerManagement';
import PrivateRoute from './components/PrivateRoute'; import PrivateRoute from './components/PrivateRoute';
import UserManagement from './components/UserManagement'; import UserManagement from './components/UserManagement';
import BackToHome from './components/BackToHome';
// Create a theme that matches the "stoer en snel" (tough and fast) requirement // Create a theme that matches the "stoer en snel" (tough and fast) requirement
const theme = createTheme({ const theme = createTheme({
@@ -55,6 +58,7 @@ function App() {
<Router> <Router>
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route <Route
path="/" path="/"
element={ element={
@@ -67,7 +71,7 @@ function App() {
path="/customers" path="/customers"
element={ element={
<PrivateRoute> <PrivateRoute>
<CustomerList /> <CustomerManagement />
</PrivateRoute> </PrivateRoute>
} }
/> />
@@ -105,6 +109,7 @@ function App() {
/> />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
<BackToHome />
</Router> </Router>
</ThemeProvider> </ThemeProvider>
); );

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Box } from '@mui/material';
import HomeIcon from '@mui/icons-material/Home';
const BackToHome = () => {
const navigate = useNavigate();
return (
<Box
sx={{
position: 'fixed',
bottom: 20,
right: 20,
zIndex: 1000,
}}
>
<Button
variant="contained"
color="primary"
startIcon={<HomeIcon />}
onClick={() => navigate('/')}
sx={{
borderRadius: '50px',
padding: '10px 20px',
boxShadow: 3,
'&:hover': {
transform: 'scale(1.05)',
transition: 'transform 0.2s ease-in-out',
},
}}
>
Home
</Button>
</Box>
);
};
export default BackToHome;

View File

@@ -18,12 +18,57 @@ import {
Paper, Paper,
IconButton, IconButton,
Alert, Alert,
MenuItem,
Select,
FormControl,
InputLabel,
} from '@mui/material'; } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import axios from 'axios'; import axios from 'axios';
const carModels = [
{ value: 'bmw_m3', label: 'BMW M3' },
{ value: 'bmw_m4', label: 'BMW M4' },
{ value: 'audi_rs3', label: 'Audi RS3' },
{ value: 'audi_rs4', label: 'Audi RS4' },
{ value: 'mercedes_c63', label: 'Mercedes-AMG C63' },
{ value: 'mercedes_e63', label: 'Mercedes-AMG E63' },
{ value: 'volkswagen_golf_r', label: 'Volkswagen Golf R' },
{ value: 'volkswagen_arteon_r', label: 'Volkswagen Arteon R' },
{ value: 'toyota_supra', label: 'Toyota Supra' },
{ value: 'nissan_gtr', label: 'Nissan GT-R' },
{ value: 'porsche_911', label: 'Porsche 911' },
{ value: 'porsche_cayman', label: 'Porsche Cayman' },
{ value: 'honda_civic_type_r', label: 'Honda Civic Type R' },
{ value: 'subaru_wrx_sti', label: 'Subaru WRX STI' },
{ value: 'mitsubishi_evo', label: 'Mitsubishi Lancer Evolution' },
{ value: 'other', label: 'Other' },
];
// Create axios instance with default config
const api = axios.create({
baseURL: 'http://localhost:5000/api',
headers: {
'Content-Type': 'application/json',
},
});
// Add request interceptor to add token to all requests
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
const CustomerManagement = () => { const CustomerManagement = () => {
const [customers, setCustomers] = useState([]); const [customers, setCustomers] = useState([]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -45,7 +90,7 @@ const CustomerManagement = () => {
const fetchCustomers = async () => { const fetchCustomers = async () => {
try { try {
const response = await axios.get('http://localhost:5000/api/customers'); const response = await api.get('/customers');
setCustomers(response.data); setCustomers(response.data);
} catch (err) { } catch (err) {
setError('Failed to fetch customers'); setError('Failed to fetch customers');
@@ -87,10 +132,10 @@ const CustomerManagement = () => {
e.preventDefault(); e.preventDefault();
try { try {
if (selectedCustomer) { if (selectedCustomer) {
await axios.put(`http://localhost:5000/api/customers/${selectedCustomer._id}`, formData); await api.put(`/customers/${selectedCustomer._id}`, formData);
setSuccess('Customer updated successfully'); setSuccess('Customer updated successfully');
} else { } else {
await axios.post('http://localhost:5000/api/customers', formData); await api.post('/customers', formData);
setSuccess('Customer added successfully'); setSuccess('Customer added successfully');
} }
fetchCustomers(); fetchCustomers();
@@ -103,7 +148,7 @@ const CustomerManagement = () => {
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 {
await axios.delete(`http://localhost:5000/api/customers/${id}`); await api.delete(`/customers/${id}`);
setSuccess('Customer deleted successfully'); setSuccess('Customer deleted successfully');
fetchCustomers(); fetchCustomers();
} catch (err) { } catch (err) {
@@ -218,15 +263,21 @@ const CustomerManagement = () => {
multiline multiline
rows={2} rows={2}
/> />
<TextField <FormControl fullWidth margin="normal" required>
fullWidth <InputLabel>Car Model</InputLabel>
label="Car Model" <Select
name="carModel" name="carModel"
value={formData.carModel} value={formData.carModel}
onChange={handleChange} onChange={handleChange}
margin="normal" label="Car Model"
required >
/> {carModels.map((model) => (
<MenuItem key={model.value} value={model.value}>
{model.label}
</MenuItem>
))}
</Select>
</FormControl>
<TextField <TextField
fullWidth fullWidth
label="Car Year" label="Car Year"

View File

@@ -7,9 +7,9 @@ import {
Typography, Typography,
Box, Box,
} from '@mui/material'; } from '@mui/material';
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
import PeopleIcon from '@mui/icons-material/People'; import PeopleIcon from '@mui/icons-material/People';
import ChatIcon from '@mui/icons-material/Chat'; import BuildIcon from '@mui/icons-material/Build';
import HistoryIcon from '@mui/icons-material/History';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import axios from 'axios'; import axios from 'axios';
@@ -20,43 +20,38 @@ const Dashboard = () => {
useEffect(() => { useEffect(() => {
const checkAdminStatus = async () => { const checkAdminStatus = async () => {
try { try {
const response = await axios.get('http://localhost:5000/api/users/me', { const response = await axios.get('http://localhost:5000/api/users/me');
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
setIsAdmin(response.data.role === 'admin'); setIsAdmin(response.data.role === 'admin');
} catch (error) { } catch (err) {
console.error('Error checking admin status:', error); console.error('Error checking admin status:', err);
} }
}; };
checkAdminStatus(); checkAdminStatus();
}, []); }, []);
const menuItems = [ const menuItems = [
{ {
title: 'Customers', title: 'Customers',
description: 'Manage customer information and car details',
icon: <PeopleIcon sx={{ fontSize: 40 }} />, icon: <PeopleIcon sx={{ fontSize: 40 }} />,
description: 'View and manage customer information',
path: '/customers', path: '/customers',
}, },
{ {
title: 'Car Modifications', title: 'Car Modifications',
icon: <DirectionsCarIcon sx={{ fontSize: 40 }} />, description: 'Browse available car modifications and upgrades',
description: 'Browse available car modifications', icon: <BuildIcon sx={{ fontSize: 40 }} />,
path: '/modifications', path: '/modifications',
}, },
{ {
title: 'Contact History', title: 'Contact History',
icon: <ChatIcon sx={{ fontSize: 40 }} />, description: 'View and manage customer interactions',
description: 'View customer interaction history', icon: <HistoryIcon sx={{ fontSize: 40 }} />,
path: '/contacts', path: '/contacts',
}, },
...(isAdmin ? [{ ...(isAdmin ? [{
title: 'User Management', title: 'User Management',
icon: <AdminPanelSettingsIcon sx={{ fontSize: 40 }} />,
description: 'Manage system users and permissions', description: 'Manage system users and permissions',
icon: <AdminPanelSettingsIcon sx={{ fontSize: 40 }} />,
path: '/users', path: '/users',
}] : []), }] : []),
]; ];
@@ -68,7 +63,7 @@ const Dashboard = () => {
</Typography> </Typography>
<Grid container spacing={3}> <Grid container spacing={3}>
{menuItems.map((item) => ( {menuItems.map((item) => (
<Grid item xs={12} md={4} key={item.title}> <Grid item xs={12} sm={6} md={4} key={item.title}>
<Paper <Paper
sx={{ sx={{
p: 3, p: 3,
@@ -86,20 +81,20 @@ const Dashboard = () => {
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexGrow: 1, alignItems: 'center',
mb: 2,
color: 'primary.main',
}} }}
> >
{item.icon} {item.icon}
<Typography variant="h6" component="h2" sx={{ mt: 2, fontWeight: 'bold' }}> </Box>
<Typography variant="h5" component="h2" gutterBottom>
{item.title} {item.title}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, textAlign: 'center' }}> <Typography variant="body1" color="text.secondary">
{item.description} {item.description}
</Typography> </Typography>
</Box>
</Paper> </Paper>
</Grid> </Grid>
))} ))}

View File

@@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Container,
Paper,
Typography,
TextField,
Button,
Box,
Alert,
} from '@mui/material';
import axios from 'axios';
const Register = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setSuccess('');
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return;
}
try {
const response = await axios.post('http://localhost:5000/api/users/register', {
name: formData.name,
email: formData.email,
password: formData.password,
});
setSuccess('Registration successful! Redirecting to login...');
setTimeout(() => {
navigate('/login');
}, 2000);
} catch (err) {
setError(err.response?.data?.message || 'Registration failed');
}
};
return (
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Paper
elevation={3}
sx={{
padding: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
}}
>
<Typography component="h1" variant="h5" sx={{ mb: 3, fontWeight: 'bold' }}>
Register
</Typography>
{error && (
<Alert severity="error" sx={{ width: '100%', mb: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ width: '100%', mb: 2 }}>
{success}
</Alert>
)}
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}>
<TextField
margin="normal"
required
fullWidth
id="name"
label="Full Name"
name="name"
autoComplete="name"
autoFocus
value={formData.name}
onChange={handleChange}
/>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
value={formData.email}
onChange={handleChange}
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="new-password"
value={formData.password}
onChange={handleChange}
/>
<TextField
margin="normal"
required
fullWidth
name="confirmPassword"
label="Confirm Password"
type="password"
id="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Register
</Button>
<Button
fullWidth
variant="text"
onClick={() => navigate('/login')}
sx={{ mt: 1 }}
>
Already have an account? Login
</Button>
</Box>
</Paper>
</Box>
</Container>
);
};
export default Register;