diff --git a/.env b/.env
index 25b659a..5478a1a 100644
--- a/.env
+++ b/.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
\ No newline at end of file
diff --git a/client/public/index.html b/client/public/index.html
index 10b4f00..05a376e 100644
--- a/client/public/index.html
+++ b/client/public/index.html
@@ -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`.
-->
+
+
+
AutoTune Pro | Car Tuning Management
diff --git a/client/src/App.js b/client/src/App.js
index f979769..d2d4d19 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -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() {
} />
} />
-
-
+ <>
+
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+
+ >
}
/>
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
- } />
diff --git a/client/src/components/CarModifications.js b/client/src/components/CarModifications.js
index f916ac5..e67469f 100644
--- a/client/src/components/CarModifications.js
+++ b/client/src/components/CarModifications.js
@@ -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
+ {error && {error} }
+ {success && {success} }
+
{
{filteredModifications.map((mod) => (
-
+
{
+
+ }
+ onClick={() => handleAddToCustomer(mod)}
+ >
+ Add to Customer
+
+
))}
+
+
+ Add Modification to Customer
+
+
+
+ Select Customer
+ setSelectedCustomer(e.target.value)}
+ label="Select Customer"
+ >
+ {customers.map((customer) => (
+
+ {customer.name} - {customer.carModel} ({customer.carYear})
+
+ ))}
+
+
+
+
+
+ Cancel
+
+ Add to Customer
+
+
+
);
};
diff --git a/client/src/components/CustomerManagement.js b/client/src/components/CustomerManagement.js
index 78bb3b0..1d7dffe 100644
--- a/client/src/components/CustomerManagement.js
+++ b/client/src/components/CustomerManagement.js
@@ -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
/>
+
+
+ Car Modifications
+ mod._id)}
+ onChange={handleModificationsChange}
+ label="Car Modifications"
+ renderValue={(selected) =>
+ modifications
+ .filter((mod) => selected.includes(mod._id))
+ .map((mod) => mod.name)
+ .join(', ')
+ }
+ >
+ {modifications.map((mod) => (
+
+ {mod.name} ({mod.category})
+
+ ))}
+
+
+
diff --git a/client/src/components/Header.js b/client/src/components/Header.js
new file mode 100644
index 0000000..6d8aeaf
--- /dev/null
+++ b/client/src/components/Header.js
@@ -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 (
+
+
+
+ Xatec CRM
+
+
+
+ Logout
+
+
+
+
+ );
+};
+
+export default Header;
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
index ec2585e..22bf2db 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -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;
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index a59cef2..9c4ed3c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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:
diff --git a/models/Modification.js b/models/Modification.js
new file mode 100644
index 0000000..8f3ff81
--- /dev/null
+++ b/models/Modification.js
@@ -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;
\ No newline at end of file
diff --git a/routes/modifications.js b/routes/modifications.js
new file mode 100644
index 0000000..53ba956
--- /dev/null
+++ b/routes/modifications.js
@@ -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;
\ No newline at end of file
diff --git a/server.js b/server.js
index ee281c2..9d50465 100644
--- a/server.js
+++ b/server.js
@@ -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}`));
\ No newline at end of file