mirror of
https://github.com/Alvin-Zilverstand/challenge-11.git
synced 2026-03-06 02:56:27 +01:00
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:
4
.env
4
.env
@@ -1,3 +1,3 @@
|
||||
MONGODB_URI=mongodb://localhost:27017/car-tuning-crm
|
||||
JWT_SECRET=your-secret-key
|
||||
MONGODB_URI=mongodb://root:password@localhost:27017/car-tuning-crm
|
||||
JWT_SECRET=password
|
||||
PORT=5000
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
33
client/src/components/Header.js
Normal file
33
client/src/components/Header.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ services:
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=root
|
||||
- MONGO_INITDB_ROOT_PASSWORD=password
|
||||
- MONGO_INITDB_DATABASE=car-tuning-crm
|
||||
|
||||
volumes:
|
||||
|
||||
14
models/Modification.js
Normal file
14
models/Modification.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const modificationSchema = new mongoose.Schema({
|
||||
name: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
price: { type: Number, required: true },
|
||||
category: { type: String, required: true },
|
||||
icon: { type: String }, // optional: path to SVG/icon
|
||||
createdAt: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
const Modification = mongoose.model('Modification', modificationSchema);
|
||||
|
||||
module.exports = Modification;
|
||||
27
routes/modifications.js
Normal file
27
routes/modifications.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Modification = require('../models/Modification');
|
||||
const auth = require('../middleware/auth');
|
||||
|
||||
// Get all modifications
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const modifications = await Modification.find().sort({ name: 1 });
|
||||
res.json(modifications);
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Add a new modification (admin only, or for seeding)
|
||||
router.post('/', auth, async (req, res) => {
|
||||
try {
|
||||
const mod = new Modification(req.body);
|
||||
await mod.save();
|
||||
res.status(201).json(mod);
|
||||
} catch (err) {
|
||||
res.status(400).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -24,6 +24,7 @@ app.use('/api/auth', require('./routes/auth'));
|
||||
app.use('/api/users', require('./routes/users'));
|
||||
app.use('/api/customers', require('./routes/customers'));
|
||||
app.use('/api/contacts', require('./routes/contacts'));
|
||||
app.use('/api/modifications', require('./routes/modifications'));
|
||||
|
||||
const PORT = process.env.PORT || 5000;
|
||||
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
||||
Reference in New Issue
Block a user