Enhance CarModifications component by integrating API calls for fetching modifications and customers, implementing a dialog for adding modifications to customers, and improving error and success message handling. Refactor state management and UI elements for better user experience.

This commit is contained in:
Alvin
2025-06-12 09:38:03 +02:00
parent 1ac852f2a1
commit f66670137b
11 changed files with 516 additions and 234 deletions

View File

@@ -24,6 +24,9 @@
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`.
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>AutoTune Pro | Car Tuning Management</title>
</head>
<body>

View File

@@ -13,38 +13,126 @@ import CustomerManagement from './components/CustomerManagement';
import PrivateRoute from './components/PrivateRoute';
import UserManagement from './components/UserManagement';
import BackToHome from './components/BackToHome';
import Header from './components/Header';
// Create a theme that matches the "stoer en snel" (tough and fast) requirement
// Create a dark, modern theme that matches xatec.nl's style
const theme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#ff3d00', // Bright orange for speed and energy
main: '#E02D1B', // Darker vibrant red
light: '#ff5a4d',
dark: '#A81F0A',
},
secondary: {
main: '#212121', // Dark gray for toughness
main: '#ffffff', // White for contrast
},
background: {
default: '#121212',
paper: '#1e1e1e',
default: '#111111', // True black background
paper: '#181818', // Slightly lighter for cards
},
text: {
primary: '#ffffff',
secondary: '#b3b3b3',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
fontFamily: 'Inter, Roboto, Helvetica, Arial, sans-serif',
h1: {
fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif',
fontWeight: 700,
fontSize: '2.7rem',
color: '#ffffff',
letterSpacing: '0.07em',
textTransform: 'uppercase',
},
h2: {
fontWeight: 700,
fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif',
fontWeight: 600,
fontSize: '2.1rem',
color: '#ffffff',
letterSpacing: '0.06em',
textTransform: 'uppercase',
},
h3: {
fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif',
fontWeight: 600,
fontSize: '1.8rem',
color: '#ffffff',
letterSpacing: '0.05em',
textTransform: 'uppercase',
},
body1: {
fontFamily: 'Inter, Roboto, Helvetica, Arial, sans-serif',
fontSize: '1.08rem',
lineHeight: 1.7,
color: '#ffffff',
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 0,
textTransform: 'none',
fontWeight: 600,
borderRadius: '4px',
textTransform: 'uppercase',
fontWeight: 700,
fontSize: '1.08rem',
padding: '14px 32px',
backgroundColor: '#E02D1B',
color: '#fff',
boxShadow: '0 2px 12px rgba(224,45,27,0.10)',
letterSpacing: '0.08em',
margin: '8px 0',
transition: 'box-shadow 0.2s, background 0.2s',
'&:hover': {
backgroundColor: '#A81F0A',
boxShadow: '0 4px 20px rgba(224,45,27,0.18)',
},
},
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: '4px',
boxShadow: '0 4px 24px rgba(0,0,0,0.35)',
backgroundColor: '#181818',
padding: '32px 28px',
margin: '24px 0',
},
},
},
MuiAppBar: {
styleOverrides: {
root: {
backgroundColor: '#181818',
color: '#ffffff',
boxShadow: '0 2px 8px rgba(0,0,0,0.25)',
},
},
},
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: '#333333',
},
'&:hover fieldset': {
borderColor: '#E02D1B',
},
'&.Mui-focused fieldset': {
borderColor: '#E02D1B',
},
},
},
},
},
MuiTableCell: {
styleOverrides: {
root: {
borderBottom: '1px solid #222',
padding: '18px 12px',
},
},
},
@@ -60,54 +148,64 @@ function App() {
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/"
path="*"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
<>
<Header />
<Routes>
<Route
path="/"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/customers"
element={
<PrivateRoute>
<CustomerManagement />
</PrivateRoute>
}
/>
<Route
path="/customers/:id"
element={
<PrivateRoute>
<CustomerDetail />
</PrivateRoute>
}
/>
<Route
path="/modifications"
element={
<PrivateRoute>
<CarModifications />
</PrivateRoute>
}
/>
<Route
path="/contacts"
element={
<PrivateRoute>
<ContactHistory />
</PrivateRoute>
}
/>
<Route
path="/users"
element={
<PrivateRoute>
<UserManagement />
</PrivateRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</>
}
/>
<Route
path="/customers"
element={
<PrivateRoute>
<CustomerManagement />
</PrivateRoute>
}
/>
<Route
path="/customers/:id"
element={
<PrivateRoute>
<CustomerDetail />
</PrivateRoute>
}
/>
<Route
path="/modifications"
element={
<PrivateRoute>
<CarModifications />
</PrivateRoute>
}
/>
<Route
path="/contacts"
element={
<PrivateRoute>
<ContactHistory />
</PrivateRoute>
}
/>
<Route
path="/users"
element={
<PrivateRoute>
<UserManagement />
</PrivateRoute>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<BackToHome />
</Router>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
Container,
Typography,
@@ -6,10 +6,23 @@ import {
Paper,
Card,
CardContent,
Box,
CardActions,
Button,
TextField,
InputAdornment,
Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
InputLabel,
Select,
MenuItem,
Alert
} from '@mui/material';
import { Add as AddIcon } from '@mui/icons-material';
import axios from 'axios';
import SearchIcon from '@mui/icons-material/Search';
// Import SVG icons
@@ -19,180 +32,88 @@ import suspensionIcon from '../assets/icons/suspension.svg';
import brakesIcon from '../assets/icons/brakes.svg';
import wheelsIcon from '../assets/icons/wheels.svg';
// Create axios instance with default config
const api = axios.create({
baseURL: 'http://localhost:5000/api'
});
// Add request interceptor to include token
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 CarModifications = () => {
const [modifications, setModifications] = useState([]);
const [customers, setCustomers] = useState([]);
const [selectedCustomer, setSelectedCustomer] = useState('');
const [selectedModification, setSelectedModification] = useState(null);
const [openDialog, setOpenDialog] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [searchTerm, setSearchTerm] = useState('');
// Sample modifications data - in a real app, this would come from an API
const modifications = [
{
id: 1,
name: 'Performance Chip',
description: 'Increase engine power and torque with our custom ECU tuning',
price: '€299',
icon: engineIcon,
category: 'Engine',
},
{
id: 2,
name: 'Sport Exhaust System',
description: 'High-flow exhaust system for better sound and performance',
price: '€599',
icon: exhaustIcon,
category: 'Exhaust',
},
{
id: 3,
name: 'Lowering Springs',
description: 'Sport suspension lowering springs for improved handling',
price: '€399',
icon: suspensionIcon,
category: 'Suspension',
},
{
id: 4,
name: 'Cold Air Intake',
description: 'Improved air flow for better engine performance',
price: '€199',
icon: engineIcon,
category: 'Engine',
},
{
id: 5,
name: 'Sport Brake Kit',
description: 'Upgraded brake system for better stopping power',
price: '€899',
icon: brakesIcon,
category: 'Brakes',
},
{
id: 6,
name: 'Wheel Spacers',
description: 'Improve stance and handling with wheel spacers',
price: '€149',
icon: wheelsIcon,
category: 'Wheels',
},
{
id: 7,
name: 'Turbocharger Kit',
description: 'Complete turbo upgrade kit for significant power gains',
price: '€2499',
icon: engineIcon,
category: 'Engine',
},
{
id: 8,
name: 'Cat-Back Exhaust',
description: 'Performance exhaust system with sport sound',
price: '€799',
icon: exhaustIcon,
category: 'Exhaust',
},
{
id: 9,
name: 'Coilover Suspension',
description: 'Fully adjustable suspension system for perfect handling',
price: '€1299',
icon: suspensionIcon,
category: 'Suspension',
},
{
id: 10,
name: 'Big Brake Kit',
description: '6-piston caliper upgrade with larger rotors',
price: '€1499',
icon: brakesIcon,
category: 'Brakes',
},
{
id: 11,
name: 'Forged Wheels',
description: 'Lightweight forged alloy wheels for better performance',
price: '€1999',
icon: wheelsIcon,
category: 'Wheels',
},
{
id: 12,
name: 'Stage 2 Tune',
description: 'Advanced ECU remap for maximum power gains',
price: '€499',
icon: engineIcon,
category: 'Engine',
},
{
id: 13,
name: 'Downpipe',
description: 'High-flow downpipe for improved exhaust flow',
price: '€349',
icon: exhaustIcon,
category: 'Exhaust',
},
{
id: 14,
name: 'Sway Bars',
description: 'Upgraded sway bars for reduced body roll',
price: '€299',
icon: suspensionIcon,
category: 'Suspension',
},
{
id: 15,
name: 'Brake Pads',
description: 'High-performance brake pads for better stopping',
price: '€199',
icon: brakesIcon,
category: 'Brakes',
},
{
id: 16,
name: 'Wheel Bearings',
description: 'Upgraded wheel bearings for smoother rotation',
price: '€249',
icon: wheelsIcon,
category: 'Wheels',
},
{
id: 17,
name: 'Intercooler Upgrade',
description: 'Larger intercooler for better cooling efficiency',
price: '€699',
icon: engineIcon,
category: 'Engine',
},
{
id: 18,
name: 'Exhaust Manifold',
description: 'Equal-length exhaust manifold for better flow',
price: '€449',
icon: exhaustIcon,
category: 'Exhaust',
},
{
id: 19,
name: 'Strut Brace',
description: 'Front strut brace for improved chassis rigidity',
price: '€199',
icon: suspensionIcon,
category: 'Suspension',
},
{
id: 20,
name: 'Brake Lines',
description: 'Stainless steel braided brake lines',
price: '€149',
icon: brakesIcon,
category: 'Brakes',
},
{
id: 21,
name: 'Wheel Locks',
description: 'Security wheel locks to prevent theft',
price: '€89',
icon: wheelsIcon,
category: 'Wheels',
useEffect(() => {
fetchModifications();
fetchCustomers();
}, []);
const fetchModifications = async () => {
try {
const response = await api.get('/modifications');
setModifications(response.data);
} catch (err) {
setError('Failed to fetch modifications');
}
];
};
const fetchCustomers = async () => {
try {
const response = await api.get('/customers');
setCustomers(response.data);
} catch (err) {
setError('Failed to fetch customers');
}
};
const handleAddToCustomer = (modification) => {
setSelectedModification(modification);
setOpenDialog(true);
};
const handleCloseDialog = () => {
setOpenDialog(false);
setSelectedCustomer('');
setSelectedModification(null);
};
const handleSubmit = async () => {
if (!selectedCustomer) {
setError('Please select a customer');
return;
}
try {
await api.put(`/customers/${selectedCustomer}/modifications`, {
name: selectedModification.name,
description: selectedModification.description,
price: selectedModification.price,
category: selectedModification.category
});
setSuccess('Modification added to customer successfully');
handleCloseDialog();
} catch (err) {
setError(err.response?.data?.message || 'Failed to add modification to customer');
}
};
const filteredModifications = modifications.filter((mod) =>
mod.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -206,6 +127,9 @@ const CarModifications = () => {
Car Modifications
</Typography>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
{success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
<Box sx={{ mb: 4 }}>
<TextField
fullWidth
@@ -225,7 +149,7 @@ const CarModifications = () => {
<Grid container spacing={3}>
{filteredModifications.map((mod) => (
<Grid item xs={12} sm={6} md={4} key={mod.id}>
<Grid item xs={12} sm={6} md={4} key={mod._id}>
<Card
sx={{
height: '100%',
@@ -292,10 +216,48 @@ const CarModifications = () => {
</Typography>
</Box>
</CardContent>
<CardActions>
<Button
size="small"
color="primary"
startIcon={<AddIcon />}
onClick={() => handleAddToCustomer(mod)}
>
Add to Customer
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
<Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>Add Modification to Customer</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
<FormControl fullWidth>
<InputLabel>Select Customer</InputLabel>
<Select
value={selectedCustomer}
onChange={(e) => setSelectedCustomer(e.target.value)}
label="Select Customer"
>
{customers.map((customer) => (
<MenuItem key={customer._id} value={customer._id}>
{customer.name} - {customer.carModel} ({customer.carYear})
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Cancel</Button>
<Button onClick={handleSubmit} variant="contained" color="primary">
Add to Customer
</Button>
</DialogActions>
</Dialog>
</Container>
);
};

View File

@@ -78,6 +78,7 @@ api.interceptors.request.use(
const CustomerManagement = () => {
const [customers, setCustomers] = useState([]);
const [modifications, setModifications] = useState([]);
const [open, setOpen] = useState(false);
const [openModDialog, setOpenModDialog] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null);
@@ -89,7 +90,8 @@ const CustomerManagement = () => {
phone: '',
address: '',
carModel: '',
carYear: ''
carYear: '',
modifications: []
});
const [modificationData, setModificationData] = useState({
name: '',
@@ -100,6 +102,7 @@ const CustomerManagement = () => {
useEffect(() => {
fetchCustomers();
fetchModifications();
}, []);
const fetchCustomers = async () => {
@@ -111,9 +114,21 @@ const CustomerManagement = () => {
}
};
const fetchModifications = async () => {
try {
const response = await api.get('/modifications');
setModifications(response.data);
} catch (err) {
setError('Failed to fetch modifications');
}
};
const handleOpen = (customer = null) => {
if (customer) {
setFormData(customer);
setFormData({
...customer,
modifications: customer.modifications || []
});
setSelectedCustomer(customer);
} else {
setFormData({
@@ -122,7 +137,8 @@ const CustomerManagement = () => {
phone: '',
address: '',
carModel: '',
carYear: ''
carYear: '',
modifications: []
});
setSelectedCustomer(null);
}
@@ -166,6 +182,15 @@ const CustomerManagement = () => {
});
};
const handleModificationsChange = (e) => {
const selectedIds = e.target.value;
const selectedMods = modifications.filter((mod) => selectedIds.includes(mod._id));
setFormData({
...formData,
modifications: selectedMods
});
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
@@ -354,6 +379,30 @@ const CustomerManagement = () => {
required
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel>Car Modifications</InputLabel>
<Select
multiple
name="modifications"
value={formData.modifications.map((mod) => mod._id)}
onChange={handleModificationsChange}
label="Car Modifications"
renderValue={(selected) =>
modifications
.filter((mod) => selected.includes(mod._id))
.map((mod) => mod.name)
.join(', ')
}
>
{modifications.map((mod) => (
<MenuItem key={mod._id} value={mod._id}>
{mod.name} ({mod.category})
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
</Box>
</DialogContent>

View File

@@ -0,0 +1,33 @@
import React from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import { useNavigate } from 'react-router-dom';
const Header = () => {
const navigate = useNavigate();
const handleLogout = () => {
localStorage.removeItem('token');
navigate('/login');
};
return (
<AppBar position="static" color="transparent" elevation={0} sx={{ mb: 4, background: 'none' }}>
<Toolbar>
<Typography variant="h5" sx={{ flexGrow: 1, fontWeight: 700, letterSpacing: '0.1em', fontFamily: 'Orbitron, Inter, Roboto, Helvetica, Arial, sans-serif' }}>
Xatec CRM
</Typography>
<Box>
<Button color="primary" variant="contained" onClick={handleLogout} sx={{ fontWeight: 700, ml: 2 }}>
Logout
</Button>
</Box>
</Toolbar>
</AppBar>
);
};
export default Header;

View File

@@ -1,13 +1,106 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #111111;
color: #ffffff;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Orbitron', 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #fff;
margin-top: 0;
margin-bottom: 18px;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* Global styles for consistent spacing and layout */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 32px;
}
/* Smooth transitions */
* {
transition: background-color 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #181818;
}
::-webkit-scrollbar-thumb {
background: #E02D1B;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #A81F0A;
}
/* Selection color */
::selection {
background: #E02D1B;
color: #ffffff;
}
/* Focus outline */
:focus {
outline: 2px solid #E02D1B;
outline-offset: 2px;
}
button, .MuiButton-root {
border-radius: 4px !important;
font-family: 'Orbitron', 'Inter', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
letter-spacing: 0.08em;
background-color: #E02D1B;
color: #fff;
text-transform: uppercase;
font-size: 1.08rem;
padding: 14px 32px;
box-shadow: 0 2px 12px rgba(224,45,27,0.10);
margin: 8px 0;
transition: box-shadow 0.2s, background 0.2s;
}
button:hover, .MuiButton-root:hover {
background-color: #A81F0A;
box-shadow: 0 4px 20px rgba(224,45,27,0.18);
}
.MuiCard-root, .card {
border-radius: 4px;
background: #181818;
box-shadow: 0 4px 24px rgba(0,0,0,0.35);
padding: 32px 28px;
margin: 24px 0;
}
input, .MuiInputBase-root {
background: #181818 !important;
color: #fff !important;
border-radius: 4px !important;
}
.MuiTableCell-root {
border-bottom: 1px solid #222 !important;
padding: 18px 12px !important;
}